先记住这个答案
属性被定义成 writable: false 后,obj.x = 9 这样的赋值一定不会成功,区别只在失败时报不报错。严格模式下抛 TypeError;非严格模式下这句赋值被静默丢掉,代码继续往下跑,属性还是原值。决定行为的是执行赋值那行代码所处的作用域,和属性定义时是不是严格模式无关。需要在不抛错的前提下知道写没写成功,可以用 Reflect.set,它返回布尔值。
- 严格模式:抛
TypeError,值不变 - 非严格模式:静默失败,值不变
- 看赋值那行代码的严格性,不看定义处
- 原型上的只读属性也会挡住子对象赋值
同一次赋值,两种结局
下面把同一个只读属性分别放在非严格函数和严格函数里赋值,再用 Reflect.set 试一次。
Reflect.setJavaScriptconst obj = {}
Object.defineProperty(obj, 'x', { value: 42, writable: false })
function sloppyWrite() {
obj.x = 9 // 非严格模式:静默失败,不报错
return 'sloppy x=' + obj.x
}
function strictWrite() {
'use strict'
try {
obj.x = 9
} catch (e) {
return e.name + ' x=' + obj.x
}
}
console.log(sloppyWrite())
console.log(strictWrite())
console.log(Reflect.set(obj, 'x', 9))查看输出与解释
sloppy x=42
TypeError x=42
false三种情况下 x 都还是 42,写入要么完全成功,要么完全无效,不存在写了一半的状态。
为什么会有两种表现?早期 JavaScript 的设计是尽量让脚本跑下去,很多错误都选择了静默失败。严格模式是后来加的,专门把这类静默错误变成显式异常。所以现在的建议很简单:代码统一跑在严格模式下。好在 ES module 和 class 内部默认就是严格模式,用现代构建工具写的业务代码基本都已经是严格的了。
- 静默失败为什么危险
- 比如某个 SDK 把默认超时锁成只读,业务里有一个没开严格模式的老脚本写了
config.timeout = 500。这句不报错,开发者以为改成了 500 毫秒,实际请求还是按原值超时,排查时很难想到是这行赋值根本没生效。换成严格模式,同样的代码在第一次运行时就会抛错。
原型上的只读属性也会挡住赋值
这是面试里区分度最高的一点。正常情况下给对象赋值,如果自身没有这个属性,会直接在对象上新建一个,遮住原型上的同名属性。但如果原型链上找到的同名属性是只读的,赋值就会失败,不会新建自有属性。
defineProperty 可以JavaScriptconst proto = {}
Object.defineProperty(proto, 'id', { value: 1, writable: false })
const child = Object.create(proto)
child.id = 2 // 非严格模式下静默失败
console.log(child.id, Object.hasOwn(child, 'id'))
Object.defineProperty(child, 'id', { value: 2 }) // 绕开赋值,直接定义自有属性
console.log(child.id, Object.hasOwn(child, 'id'))查看输出与解释
1 false
2 true赋值走的是「先沿原型链找同名属性,再决定怎么写」的流程,找到只读属性就停了。defineProperty 不走这套流程,它只管在 child 自身上定义属性,所以能成功。
这个行为在实际项目里会踩到:有人对 Object.prototype 或某个公共原型做了 Object.freeze,之后所有继承它的对象都没法用赋值的方式添加同名属性,比如 toString。
容易答错的地方
- 以为定义属性时开了严格模式就行
- 严格性看的是执行赋值的那行代码。定义属性的模块是严格的,但调用方是非严格脚本,赋值照样静默失败。
- 以为
writable: false就彻底改不了了 - 它只挡赋值。如果属性是
configurable: true,用Object.defineProperty(obj, 'x', { value: 9 })一样能把值改掉。要彻底锁住,得同时设configurable: false。注意defineProperty没写configurable时默认就是false,上面第一段代码里的x其实已经锁死了,再用defineProperty换成别的值会抛TypeError。 - 以为只有数据属性会这样
- 只有
get没有set的访问器属性,赋值同样会失败,规则一样:严格模式抛错,非严格模式静默失败。给不可扩展对象添加新属性也属于同一类失败。
面试官还会怎么问?
非严格模式下怎么知道写入失败了?
用 Reflect.set(obj, key, value),写成功返回 true,失败返回 false,不依赖异常。也可以写完后再读一次对比,但不如 Reflect.set 直接。
Object.freeze 和 writable: false 是什么关系?
Object.freeze 会把对象所有自有数据属性都设成 writable: false 和 configurable: false,再把对象设为不可扩展。所以给冻结对象的属性赋值,表现和这题完全一样。它是浅冻结,嵌套对象的属性还能改。
数组的 length 设成只读会怎样?
push 这类会修改 length 的方法会抛 TypeError,而且不管调用处是不是严格模式都会抛,因为 push 内部是按严格语义去写 length 的。直接给 length 赋值则遵循本题的严格或非严格规则。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。