Object.defineProperty与原型直接赋值的行为差异及疑问
前置代码
class Something { a() { } get b() { return "bValue" } } Something.prototype.eef = "dfd"; window.someValue = "parent value"; Object.defineProperty(Something.prototype,"abc",{ enumerable:true, configurable:true, get:function(){ debugger; return "parent value"; }, set : function(value){ debugger; window.someValue=value; } }); class Derived extends Something { get b() { return "inside child bValue" } } class Derived1 extends Something{ }; /* obj1=new Derived1(); obj1.abc 应该返回 "parent value" */
测试场景
场景1:直接在Derived类原型上设置abc属性
Derived.prototype.abc="child value"; /* obj2=new Derived(); obj2.abc 预期返回"child value",实际得到"parent value" */
场景2:使用Object.defineProperty在Derived类原型上设置abc属性
Object.defineProperty(Derived.prototype,"abc",{ value:"child value", enumerable:true, configurable:true }); /* obj3=new Derived(); obj3.abc 返回"child value",符合预期 */
问题描述
我通过上述两种方式在Derived类的原型上定义abc属性,预期创建Derived实例后,两种场景下获取obj.abc都应返回"child value"。但实际测试发现,场景1会触发父类Something原型中abc的getter,得到"parent value",场景2则符合预期。我无法理解为何直接赋值会触发父类的存取器,以及如何避免通过子类修改父类原型。
问题解答
为什么直接赋值会触发父类的存取器?
这是JavaScript属性赋值的规则导致的:当你给对象的某个属性赋值时,引擎会先检查对象自身是否存在该属性。如果没有,会顺着原型链向上查找,直到找到第一个包含该属性的对象。如果找到的属性是带setter的存取器属性,引擎会直接调用这个setter方法,不会在当前对象(这里是Derived.prototype)上新增同名属性。
在场景1中,Derived.prototype本身没有abc属性,向上查找会找到Something.prototype上的abc(带有get/set的存取器),所以执行Derived.prototype.abc = "child value"时,实际是调用了父类原型的set方法,并没有在子类原型上添加新的abc属性。后续访问obj2.abc时,依然会沿着原型链找到父类的getter,返回"parent value"。
而场景2使用Object.defineProperty时,是直接在Derived.prototype上定义了一个数据属性(带有value字段),这个属性会覆盖原型链上层的同名属性,所以实例访问时会优先读取子类原型的属性值。
如何避免通过子类修改父类原型?
- 使用
Object.defineProperty定义子类原型属性:如场景2所示,直接在子类原型上定义数据属性,既不会触发父类的存取器,也不会修改父类原型的内容。 - 在子类类体中定义属性:如果是类结构,可以直接在子类内部通过存取器或数据属性定义:
class Derived extends Something { get b() { return "inside child bValue" } // 定义子类自己的abc存取器 get abc() { return "child value"; } // 或者定义实例数据属性(注意:这是实例属性,非原型属性) abc = "child value"; }
如果需要定义原型属性,还是推荐使用Object.defineProperty操作子类原型。
3. 确保父类原型属性的configurable为true:只有父类属性设置了configurable: true,子类才能在自己的原型上定义同名属性进行覆盖(场景2能成功就是因为父类的abc满足这个条件)。如果父类属性是configurable: false,子类无法覆盖该属性,也不会意外修改父类原型。
内容的提问来源于stack exchange,提问作者jay102

