1. 从一次深夜调试说起当属性拒绝被写入凌晨两点屏幕上的光标在控制台的红字错误上闪烁这大概是每个前端开发者都经历过的“美妙”时刻。错误信息简洁而冷酷Uncaught TypeError: Cannot set property xxx which has only a getter。你盯着这行字心里可能在想“我明明定义了这个对象也给它赋值了为什么说它只有 getter” 或者更常见的是你正在使用一个第三方库比如 Vue 3 的响应式对象或者某个 UI 框架的组件实例然后试图修改一个看似普通的属性结果被这个错误当头一棒。这个错误的核心直指 JavaScript 对象属性描述符Property Descriptor中的一个关键特性属性的可写性writability。它不是一个 Bug而是 JavaScript 语言机制对你代码意图的一次“合规性检查”。简单来说你试图向一个被设计为“只读”的属性写入新值JavaScript 引擎礼貌但坚决地阻止了你。理解这个错误不仅是解决眼前的问题更是深入理解 JavaScript 对象模型、现代框架响应式原理乃至编写更健壮代码的绝佳契机。本文将彻底拆解这个异常从底层原理到高频实战场景手把手带你定位问题、理解成因并提供一套完整的解决方案和避坑指南。无论你是遇到了 Vue 3 的reactive对象、使用了Object.defineProperty定义的属性还是不小心修改了 DOM 或内置对象的只读属性这里都有对应的答案。2. 属性描述符理解错误的基石要真正搞懂Cannot set property which has only a getter我们必须先回到 JavaScript 对象的基础——属性描述符。在 JavaScript 中对象的属性远不止一个名字和一个值那么简单。每个属性都关联着一个“属性描述符”对象它定义了该属性的元数据和行为。2.1 数据属性与存取器属性属性描述符主要分为两大类数据属性和存取器属性。这是理解整个问题的关键分水岭。数据属性包含一个值并且可以通过四个特性来描述其行为value: 属性的值。writable: 布尔值表示属性的值是否可以被改变。enumerable: 布尔值表示属性是否可以在for...in循环或Object.keys()中被枚举。configurable: 布尔值表示属性是否可以被删除或者其特性除了value和writable是否可以被修改。当你用最常见的点号或方括号语法定义一个属性时你创建的就是一个数据属性并且writable、enumerable、configurable默认都为true。let obj {}; obj.name Alice; // 默认创建了一个可写、可枚举、可配置的数据属性 // 等价于内部实现不可直接书写 Object.defineProperty(obj, name, { value: Alice, writable: true, enumerable: true, configurable: true });存取器属性则不包含数据值而是由一对函数——getter和setter——来定义。它也有四个特性但含义不同get: 一个函数在读取属性时调用返回值即为属性值。set: 一个函数在写入属性时调用接收一个参数即要赋予的值。enumerable: 同上。configurable: 同上。存取器属性允许你在读取或写入属性时执行自定义逻辑这是实现计算属性、数据验证、响应式系统的核心机制。let person { _age: 25, // 约定俗成的“私有”变量实际并非真正私有 get age() { console.log(读取了 age); return this._age; }, set age(newVal) { console.log(设置了 age); if (newVal 0 newVal 150) { this._age newVal; } else { console.error(年龄无效); } } }; console.log(person.age); // 输出: 读取了 age \n 25 person.age 30; // 输出: 设置了 age console.log(person.age); // 输出: 读取了 age \n 30 person.age -5; // 输出: 设置了 age \n 年龄无效2.2 “Only a getter” 状态的产生现在让我们聚焦于错误信息中的which has only a getter。这个状态是如何产生的当一个属性被定义为只有get函数而没有set函数时它就成为了一个严格的只读属性。尝试给这个属性赋值JavaScript 引擎在严格模式下会抛出TypeError在非严格模式下则会静默失败赋值操作无效但不会报错。现代开发环境如 ES6 模块、Vue/React 项目默认或普遍使用严格模式因此你几乎总会看到这个错误。产生“只有 getter”属性的常见方式有显式使用Object.defineProperty或Object.defineProperties定义let obj {}; Object.defineProperty(obj, readOnlyProp, { get() { return I am read-only; }, enumerable: true // 注意这里没有定义 set }); obj.readOnlyProp new value; // Uncaught TypeError!在对象字面量中只定义 getterlet obj { get computedValue() { return Math.random(); } // 没有对应的 set computedValue(val) { ... } }; obj.computedValue 0.5; // Uncaught TypeError!某些 API 或框架返回的对象这是最常踩坑的地方。例如Vue 3 的computed计算属性如果不显式提供setter默认就是只读的。某些库的内部状态对象出于封装考虑只暴露了 getter。浏览器某些只读的 DOM 属性如window.location.href部分写入操作是通过方法如assign()完成的而非直接赋值。注意这里有一个非常重要的细微差别。writable: false的数据属性和一个只有 getter 的存取器属性在表现上都是“只读”的但错误信息不同。尝试修改writable: false的属性错误信息是Cannot assign to read only property ‘xxx’。而我们的主角错误Cannot set property ‘xxx’ which has only a getter特指后者——存取器属性缺少 setter。理解这一点有助于快速定位问题根源。3. 高频实战场景排查与修复理论清楚了我们进入实战。下面我将列举几个最常遇到此错误的场景并给出具体的排查步骤和解决方案。3.1 场景一Vue 3 的响应式系统Vue 3 的reactive()和ref()创建了代理对象其属性访问被拦截。computed()创建的计算属性默认是只读的这是此错误在 Vue 3 项目中的头号来源。问题复现import { reactive, computed } from vue; const state reactive({ count: 0 }); // 创建一个只读的计算属性 const doubleCount computed(() state.count * 2); console.log(doubleCount.value); // 0 doubleCount.value 4; // Uncaught TypeError: Cannot set property value which has only a getter你试图直接修改doubleCount.value但computed返回的Ref对象其.value属性在没有定义 setter 时是只读的。解决方案为计算属性提供 setter如果你的计算属性需要被修改通常用于实现“双向绑定”的表单元素可以传入一个带有get和set函数的对象。const writableComputed computed({ get() { return state.count * 2; }, set(newValue) { // 反向推导出源数据的值并更新 state.count newValue / 2; } }); writableComputed.value 10; // 成功state.count 变为 5检查你是否误用了computed问问自己这个值真的是基于其他响应式数据计算而来的吗如果它是一个独立的状态你应该使用ref()。// 错误一个独立的状态用了 computed const message computed(() Hello); // 只读的 // 正确使用 ref const message ref(Hello); // 可写的 message.value World; // 成功检查 reactive 对象的来源如果你是从props解构出来的属性或者是从一个readonly()包装的对象中取出的属性它们也可能是只读的。Vue 3 的toRefs在解构props时会保持其只读性。import { toRefs } from vue; export default { props: [title], setup(props) { const { title } toRefs(props); // title 是一个只读的 ref // title.value New Title; // 错误 // 正确的做法是触发一个事件通知父组件修改或者使用内部副本 const internalTitle ref(props.title); } }3.2 场景二使用Object.defineProperty或类 Getter你可能在封装工具类、设计模式如单例或进行元编程时手动定义了只有 getter 的属性。问题复现class Logger { constructor(name) { this._name name; } get name() { return this._name.toUpperCase(); } // 忘记写 set name(val) { ... } } const log new Logger(app); console.log(log.name); // APP log.name debug; // Uncaught TypeError!解决方案补全 setter如果属性设计为可写就补上 setter。set name(newVal) { if (typeof newVal string) { this._name newVal; } } 明确设计意图如果属性确实应该是只读的例如一个由内部状态衍生出的格式化显示值或者一个不应被修改的常量标识那么你应该在代码和文档中明确这一点。错误发生在运行时是最后的防线更好的做法是在设计时就避免对它进行赋值操作。可以考虑使用readonly命名约定或在注释中说明。/**只读属性返回大写的名称。readonly */ get formattedName() { return this._name.toUpperCase(); }使用Object.defineProperty定义数据属性如果你想要的是一个简单的、不可写的值使用数据属性并设置writable: false会更清晰错误信息也会更准确Cannot assign to read only property。Object.defineProperty(obj, constantId, { value: ID_123, writable: false, enumerable: true }); 3.3 场景三第三方库或内置对象许多库为了封装内部状态会返回只有 getter 的对象。浏览器内置的某些 DOM 属性也是只读的。问题排查查阅文档这是第一步也是最重要的一步。仔细阅读你正在使用的库或 API 的官方文档确认该属性是否被设计为只读。使用控制台检查在浏览器开发者工具或 Node.js REPL 中使用Object.getOwnPropertyDescriptor方法检查属性的描述符。// 假设 libObject 是某个库返回的对象 const descriptor Object.getOwnPropertyDescriptor(libObject, someProperty); console.log(descriptor); // 输出可能为{ get: f, set: undefined, enumerable: true, configurable: false }如果set是undefined那它就是只读的。解决方案遵循 API 约定如果文档说明它是只读的就不要尝试修改它。寻找其他提供的 API 方法来达到你的目的。例如不能直接设置window.location.href来导航在某些浏览器下可能允许但行为不一致而应该使用window.location.assign(‘newUrl’)。创建副本如果你需要修改这些数据可以先将其复制到一个普通对象中。const libData someLibrary.getReadOnlyData(); // 返回一个只读代理/对象 // 错误libData.item new // 正确 const mutableCopy JSON.parse(JSON.stringify(libData)); // 深拷贝注意性能和数据类型限制 mutableCopy.item new; // 或者使用扩展运算符浅拷贝 const shallowCopy { ...libData }; shallowCopy.item new;与库作者沟通如果你认为某个属性应该可写并且有充分的理由可以考虑向库的项目提 Issue 或 Feature Request。3.4 场景四原型链上的属性有时错误可能发生在对象的原型prototype上。如果你尝试修改一个从原型继承来的、只有 getter 的属性也会触发此错误。问题复现function Parent() {} Object.defineProperty(Parent.prototype, readOnlyFromParent, { get() { return ‘from parent’; } }); function Child() {} Child.prototype Object.create(Parent.prototype); const child new Child(); console.log(child.readOnlyFromParent); // ‘from parent’ child.readOnlyFromParent ‘new’; // Uncaught TypeError!这里readOnlyFromParent是定义在Parent.prototype上的存取器属性child对象通过原型链继承了它。直接对child赋值并不会在child自身上创建新属性因为属性是继承的且没有 setter而是会触发原型上 getter 的 set 操作由于没有 setter 而报错。解决方案在实例自身定义同名属性如果你需要在子类或实例中拥有一个可写的同名属性你需要在实例自身上定义它这会“遮蔽”原型链上的属性。Object.defineProperty(child, readOnlyFromParent, { value: ‘my own value’, writable: true }); child.readOnlyFromParent ‘new value’; // 成功修改的是自身属性 console.log(child.readOnlyFromParent); // ‘new value’重新设计原型考虑这个只读属性在原型设计上是否合理。如果子类普遍需要修改它或许它不应该被放在原型上作为只读属性。4. 系统化的调试与诊断流程当错误发生时不要盲目猜测。遵循一个系统化的流程可以快速定位问题。4.1 第一步精确定位错误发生的位置浏览器控制台或 Node.js 的错误栈跟踪Stack Trace是你的第一线索。点击错误信息旁边的文件名和行号直接跳转到源代码中出错的那一行。确认是哪一行代码在对哪个对象的哪个属性进行赋值。4.2 第二步检查属性描述符在错误发生前的代码处设置断点或直接在控制台中执行const obj /* 你的对象 */; const propName ‘xxx’; // 出错的属性名 console.log(Object.getOwnPropertyDescriptor(obj, propName)); // 或者更详细地查看自身和原型链上的属性 console.log(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), propName));观察输出如果有get而没有set那就是根源。如果writable为false那是另一种只读错误。如果返回undefined说明这个属性可能不存在于该对象自身需要检查原型链。4.3 第三步追溯对象来源弄清楚这个对象是从哪里来的是你自己创建的吗回顾创建对象的代码检查Object.defineProperty、类 getter 或对象字面量中的定义。是函数参数传入的吗查看函数调用处传入的是什么。可能是父组件传入的propsVue或者是其他模块返回的只读对象。是 API 调用返回的吗查阅该 API框架 API、第三方库 API、浏览器原生 API的文档。4.4 第四步判断设计意图根据对象来源判断该属性被设计为只读是否是合理的。合理应避免修改计算属性、派生状态、常量标识、封装后的内部状态、DOM 只读属性。解决方案是改变你的代码逻辑不修改它。不合理或存疑你认为它应该是可写的。这时可以检查是否有其他可写的类似属性或者考虑是否应该使用该对象的副本进行操作。4.5 一个综合排查案例假设你在一个 Vue 3 组件中遇到错误Cannot set property ‘filteredList’ which has only a getter。定位错误栈指向MyComponent.vue第 45 行this.filteredList newList;。检查在控制台对this执行Object.getOwnPropertyDescriptor(this, ‘filteredList’)发现set为undefined。追溯在组件中搜索filteredList发现它是在computed中定义的computed: { filteredList() { return this.sourceList.filter(item item.active); } }判断这是一个计算属性其值由sourceList和过滤条件决定。直接修改filteredList在逻辑上是不通的。正确的做法应该是修改源数据sourceList或过滤条件。解决将赋值语句改为修改源数据this.sourceList newSourceList;或者如果需要一个独立的可写状态就不要用computed改用ref或reactive来管理filteredList状态。5. 进阶与响应式系统及代理的关联在现代前端开发中我们很少直接操作原生对象更多的是与响应式系统如 Vue 3 的reactive MobX创建的代理Proxy对象打交道。这些系统可能会改变错误的表现形式。Vue 3 的reactive()返回一个 Proxy 对象。当你访问一个不存在的属性或者尝试修改一个只有 getter 的计算属性时Proxy 的陷阱trap会拦截这些操作并抛出更友好的错误但根源相同。理解 Proxy 有助于你理解这些框架的行为。模拟一个简单的只读代理const target { get readOnly() { return ‘original’; } }; const handler { get(obj, prop) { console.log(Getting ${prop}); return obj[prop]; }, set(obj, prop, value) { // 如果目标属性是只有 getter 的存取器属性且没有 setter const descriptor Object.getOwnPropertyDescriptor(obj, prop); if (descriptor descriptor.get !descriptor.set) { throw new TypeError(Cannot set property ${prop} which has only a getter); } // 否则允许设置这里简化了实际 Vue 的 set 陷阱更复杂 obj[prop] value; return true; } }; const proxy new Proxy(target, handler); console.log(proxy.readOnly); // Getting readOnly \n ‘original’ proxy.readOnly ‘new’; // 抛出 TypeError信息由我们的 handler 自定义在 Vue 3 中computed返回的Ref对象本身也是一个具有 getter/setter 的访问器包装器。当你访问.value时触发 getter 执行计算函数当你赋值时如果定义了 setter 就执行它没定义就抛出我们讨论的这个错误。6. 预防与最佳实践最好的错误处理是预防错误的发生。以下是一些最佳实践类型系统是好朋友使用 TypeScript 可以极大地减少这类错误。你可以为接口Interface或类型Type明确指定属性是只读的。interface AppConfig { readonly version: string; // 明确标记为只读 apiUrl: string; } const config: AppConfig { version: ‘1.0’, apiUrl: ‘/api’ }; config.version ‘2.0’; // TypeScript 编译时错误无法分配到 “version”因为它是只读属性。即使在使用 JavaScript 的项目中JSDoc 注释中的readonly标签也能给编辑器如 VSCode提供提示。善用const和Object.freeze对于不应该被重新赋值的变量使用const。对于不应该被修改的对象可以使用Object.freeze()。注意Object.freeze()是浅冻结且冻结后对象的属性描述符writable和configurable会变为false。const constants Object.freeze({ PI: 3.14159, MAX_SIZE: 100 }); constants.PI 3; // 在严格模式下静默失败或报错取决于环境框架使用守则Vue:牢记computed默认只读需要双向绑定就用v-model配合可写的ref或带 setter 的computed。谨慎解构props使用toRefs时要明白其只读性。React:React 的状态State和属性Props都是不可变的。直接修改this.state.xxx或props.xxx不会触发重新渲染且可能导致难以追踪的 Bug。总是使用setState或返回新状态/属性的函数来更新。代码审查与团队约定在团队中对于暴露给外部使用的 API、工具函数返回的对象明确哪些属性是只读的。在代码审查时注意检查对可能为只读属性的赋值操作。防御性编程在接收外部传入的对象并打算修改其属性时可以先进行检查。function safeSet(obj, prop, value) { const descriptor Object.getOwnPropertyDescriptor(obj, prop); if (descriptor descriptor.set) { obj[prop] value; } else if (descriptor descriptor.writable ! false) { obj[prop] value; } else { console.warn(Property ${prop} is read-only or non-writable.); // 或者抛出错误或者创建副本取决于业务逻辑 } }Uncaught TypeError: Cannot set property ‘xxx’ which has only a getter这个错误像一位严格的语法警察强制我们遵守对象属性的设计契约。从表面上看它阻止了一次非法的赋值操作从深层看它引导我们去思考数据流的设计、封装边界以及 API 的合理性。解决它的过程就是一次对 JavaScript 对象模型和当前所用框架或库的深入理解之旅。下次再遇到这个红字错误时希望你能会心一笑然后熟练地打开控制台开始一场有条不紊的“侦探游戏”。