为何JS类实例重写不可写属性抛TypeError,对象字面量无报错?
前言
查阅MDN文档时,我发现了JavaScript中对象属性继承的两个有趣要点。Object.defineProperty()的文档指出:
与访问器属性不同,数据属性始终设置在对象本身而非原型上。但如果继承了不可写的数据属性,仍无法在对象上修改该属性。
我的理解是,对象的任何访问器属性或不可写数据属性,除非子对象通过Object.defineProperty()静态方法显式定义该属性,否则无法被子对象重写。
此外,super还有一个特殊特性:它会在当前this对象上设置属性,相关描述如下:
设置
super的属性(如super.x = 1)的行为类似于Reflect.set(Object.getPrototypeOf(objectLiteral), "x", 1, this)。这是仅将super理解为"原型对象引用"不够准确的情况之一,因为它实际上是在this上设置属性。
但使用super同样不允许重写访问器或不可写数据属性,相关说明如下:
不过,
super.x = 1仍会检查原型对象的属性描述符,这意味着你无法重写不可写属性,且会调用对应的setter。
问题
考虑以下代码:
class A { get ap() { return this.x;} set ap(arg) { this.x = arg; } } Object.defineProperty(A.prototype, 'dp', {writable: true, value: 'writable'}); Object.defineProperty(A.prototype, 'nwdp', {writable: false, value: 'locked'}); class B extends A { method() { super.dp = 200; // 重写该属性 super.ap = 'bird'; // 不重写;调用A.prototype上的ap setter super.nwdp = 'unlocked'; // 不重写;抛出TypeError } } b = new B()
调用b.method()时,会在实例b上设置dp和x(可通过Object.getOwnPropertyNames(b)验证),但会抛出如下TypeError:
Uncaught TypeError: Cannot assign to read only property 'nwdp' of object '#B'
类似地,在对象字面量中使用super.prop = <any_value>时,大部分行为一致,但不会抛出任何错误:
let obj1 = { dp: 'writable', get ap() { return this.x; }, set ap(arg) { this.x = arg; } }; Object.defineProperty(obj1, 'nwdp', {writable: false, value: 'locked'}); let obj2 = { method() { super.dp = 200; // 重写该属性 super.ap = 'bird'; // 不重写;调用obj1上的ap setter // 不重写;但无任何报错,仿佛该语句未执行 super.nwdp = 'unlocked'; } }; Object.setPrototypeOf(obj2, obj1); obj2.method();
通过Object.getOwnPropertyNames(obj2)可看到,对象obj2上存在dp、x和method属性。
因此,核心问题是:JavaScript为何会有这种行为?
两种场景下的行为基本一致,唯独在使用super.prop = <any_value>重写子对象的不可写数据属性时,类实例会抛出TypeError,而对象字面量则似乎忽略该语句。
这种差异是有意设计的吗?背后的原因是什么?是否有相关文档记录?
解答
这种差异是ECMAScript规范的有意设计,核心原因在于严格模式的默认开启状态不同:
类的方法默认运行在严格模式下
类的所有内部方法(包括实例方法、静态方法等)默认启用严格模式。当在严格模式下执行super.nwdp = 'unlocked'时,Reflect.set操作会因为原型上的属性不可写而返回false,严格模式会将这个失败的赋值操作抛出TypeError。对象字面量的方法默认运行在非严格模式下
对象字面量中定义的方法默认处于非严格模式。此时Reflect.set操作失败返回false,但非严格模式下不会抛出错误,只是静默忽略该赋值操作。
如果给对象字面量的方法加上严格模式,行为就会和类的场景完全一致:
let obj2 = { method() { 'use strict'; super.dp = 200; super.ap = 'bird'; super.nwdp = 'unlocked'; // 此时会抛出TypeError } }; Object.setPrototypeOf(obj2, obj1); obj2.method();
规范中的核心逻辑是:super赋值操作本质上调用Reflect.set(prototype, prop, value, this),当该方法返回false时,若当前处于严格模式,则必须抛出TypeError;非严格模式下则直接忽略失败。类的方法强制开启严格模式,而对象字面量方法默认不开启,这就导致了两者的行为差异。
内容的提问来源于stack exchange,提问作者Perspicacious

