先记住这个答案
用 Object.defineProperty 把属性定义为 writable:false 后,普通赋值 obj.x = v 的结果取决于代码是否运行在严格模式。严格模式下该赋值抛出 TypeError;非严格模式下赋值被静默忽略,表达式正常结束,属性保持原值。判断边界是:只有真正执行写操作的代码所处的严格性决定行为,与属性定义时是否严格无关。可用 Object.getOwnPropertyDescriptor 检查 writable,或统一用严格模式加 try/catch 捕获非法写入。
- 严格模式写不可写属性抛 TypeError
- 非严格模式静默失败,值保持不变
- 行为由写入处代码的严格性决定
属性描述符如何拦截写入
JavaScript 中每个对象属性都带有一组描述符:数据属性包含 value、writable、enumerable、configurable。执行 obj.x = v 时,引擎先取得该属性的描述符,若 writable 为 false,内部写入操作返回失败。接下来是否抛错取决于执行赋值语句的代码是否处于严格模式:严格模式把这个失败转化为 TypeError,非严格模式直接丢弃结果。
这种分裂行为源于历史兼容。早期 JavaScript 为了让脚本尽量跑下去,大量错误被设计成静默失败;严格模式是后来加入的受限变体,专门把这类静默错误改成显式异常。因此同一句 obj.x = 9,在 'use strict' 作用域内抛错,在普通脚本里什么都不发生,属性值依旧是原值,也不存在部分写入。
const obj = {};
Object.defineProperty(obj, "x", { value: 42, writable: false });
function sloppyWrite() {
obj.x = 9;
return "sloppy done, x=" + obj.x;
}
function strictWrite() {
"use strict";
try {
obj.x = 9;
return "no error, x=" + obj.x;
} catch (e) {
return e.name + ", x=" + obj.x;
}
}
console.log(sloppyWrite());
console.log(strictWrite());
console.log(obj.x);查看输出与解释
sloppy done, x=42
TypeError, x=42
42同一属性 x,在非严格函数中赋值静默失败,在严格函数中抛出 TypeError,两种情况下 x 都保持 42。可直接在现代浏览器控制台或 Node 环境运行。
配置对象被意外改写的排查
一个 SDK 把默认超时配置定义为不可写:Object.defineProperty(config, 'timeout', { value: 3000, writable: false }),约束是任何业务代码不得改动它。某业务文件未启用严格模式,其中 config.timeout = 500 被静默吞掉,之后请求仍按 3000 毫秒超时,开发者误以为配置生效,排查半天才发现值从未改变。
处理方式是给整个代码库启用严格模式(ES module 默认严格),让同样的误写在测试阶段就抛 TypeError 暴露出来。结果:非法改写从运行期的诡异行为变成启动期即失败的明确错误。可操作判断是:凡是用 defineProperty 锁定的属性,都应假设调用方可能处于非严格环境,因此靠 writable 防篡改的同时,还要靠严格模式或 Reflect.set 的返回值来感知失败。
什么情况下这套规则会失效
第一,writable:false 只阻止修改 value,不阻止用 Object.defineProperty 重新定义该属性;要彻底锁定还需 configurable:false。第二,如果属性在原型链上是不可写数据属性,给实例赋值同名属性同样遵循严格模式规则:严格模式抛错,非严格模式静默失败、不会创建遮蔽属性,这一点常被忽略。
第三,Proxy 可以拦截 set 并返回任意结果,writable 规则不再直接生效;但规范设有不变量:目标对象存在不可配置且不可写的数据属性时,set 陷阱若返回 true 则实际值必须与目标值一致,否则抛 TypeError。代价是 Proxy 场景下不能再用描述符推断写入行为,需要改用 Reflect.set 并检查其布尔返回值来判断成败。
容易答错的地方
- 以为非严格模式赋值会部分生效
- 不会。写入要么完整成功要么完全无效,obj.x = 9 静默失败后属性值保持原值,不存在中间状态;可用 Object.getOwnPropertyDescriptor 验证 value 未变。
- 以为定义属性时开了严格模式就够了
- 行为由执行赋值语句的代码所在作用域的严格性决定,与 defineProperty 调用处的模式无关。定义处严格、写入处非严格时,写入依然静默失败。
面试官还会怎么问?
如何在非严格模式下感知写入失败?
使用 Reflect.set(obj, key, value),它返回布尔值表示写入是否成功,对 writable:false 的属性返回 false,借此可以显式处理失败而不依赖异常。
writable:false 和 configurable:false 要一起用吗?
若要属性彻底不可变应同时设置。只有 writable:false 时仍可用 Object.defineProperty 重新定义 value;加上 configurable:false 后重定义会被禁止并抛 TypeError。
对只有 getter 没有 setter 的访问器属性赋值呢?
规则相同:访问器属性缺少 set 时写入失败,严格模式抛 TypeError,非严格模式静默无效。MDN 把不可写数据属性、只读访问器、不可扩展对象新增属性并列为三类写入失败。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。