在使用JavaScript的过程当中,经过"="操做符为对象添加新属性是很常见的操做:obj.newProp = 'value';
。可是,这个操做的结果实际上会受到原型链上的同名属性影响。接下来咱们分类讨论。html
如下讨论都假设对象 自身本来不存在要赋值的属性(故称:“为对象添加新属性”)。若是对象 自身已经存在这个属性,那么这是最简单的状况,赋值行为由这个属性的 描述符(descriptor)来决定。
经过"="操做符赋值时,js引擎会沿着obj
的原型链寻找同名属性,若是最后到达原型链的尾端null
仍是没有找到同名属性,则直接在obj
上建立新属性。git
const obj = {}; obj.newProp = 'value';
这种状况很是符合人的直觉,全部js使用者应该都已经熟悉了这种状况。可是事情并非老是这么简单。程序员
沿着obj
的原型链寻找同名属性时,若是找到由data descriptor定义的同名属性,且它的writable为true,则直接在obj上建立新属性。github
const proto = { newProp: "value" }; const obj = Object.create(proto); obj.newProp = "newValue";
结果:算法
这个情形也很常见,可是对于不少人来讲可能不符合直觉:为何经过obj.newProp
能获取到原型链上的newProp属性,可是经过obj.newProp = "newValue"
却不能修改原型链上的属性而是添加新属性呢?chrome
有2个解释的理由:数组
delete obj.newProp
,而后obj.newProp就会再次表现出默认值。假设不采用这个方案,而是经过“改变默认值”,那么原来的默认值就会丢失,delete obj.newProp
不会起做用(delete操做符只会删除对象自身的属性)。沿着obj
的原型链寻找同名属性时,若是找到由data descriptor定义的同名属性,且它的writable为false,那么赋值操做失败。在这种状况下,既不会修改原型链上的同名属性,也不会为对象自身新建属性。在"strict mode"模式下会抛出错误,不然静默失败。ide
"use strict"; const proto = Object.defineProperty({}, "newProp", { value: "value", writable: false }); const obj = Object.create(proto); obj.newProp = "newValue";
在参考资料3和4中给出了这样定义的缘由:为了使getter-only property(只定义了getter而没定义setter的属性)和non-writable property具备一样的表现:函数
const a = Object.defineProperty({}, "x", { value: 1, writable: false }); const b = Object.create(a); b.x = 2; // 赋值失败
应该等价于性能
const a = { get x() { return 1; } }; const b = Object.create(a); b.x = 2; // 赋值失败,这种状况会在下面讨论到
由于原型链上的getter-only property会阻止子代对象经过"="操做符增长同名属性(稍后会讨论这种状况),因此原型链上的non-writable property也应该阻止子代对象经过"="操做符增长同名属性。
此外,参考资料1还给出了一个缘由,那就是为了模仿传统类继承语言的表现。JavaScript的继承,从表面上看,应该像是“将父类的全部属性都拷贝到了子类上”同样。所以,父对象上的属性(writable、non-writable)理应对子对象产生影响(若是子对象没有覆盖这个属性的话)。
沿着obj
的原型链寻找同名属性时,若是找到由accessor descriptor定义的同名属性,则由这个accessor descriptor中的setter来决定作什么。setter将会被调用,this指向被赋值的对象obj
(而不是setter所在的原型对象)。
若是这个accessor descriptor中只定义了getter而没有setter,则赋值操做失败,在"strict mode"模式下会抛出错误,不然静默失败。
const a = { get x() { return this._x; }, set x(v) { // 这里的this将指向b对象 this._x = v + 1; } }; const b = Object.create(a); b.x = 2; console.log(b.x); // 3 console.log(b.hasOwnProperty("_x")); // true,证实了setter中的this指向被赋值对象,而不是setter所在的原型对象
在上面的图中须要注意一点,虽然在b对象下显示了"x"属性,但这个属性实际是存在于b.__proto__上的(b.hasOwnProperty('x')
将返回false),chrome的控制台为了方便debug,将原型链上的getter属性与对象自身的属性放在一块儿展现。
为了加强“继承”和“getter/setter”的威力。假如原型对象上的setter对后代对象的赋值无效、原型对象上的getter对后代对象的取值无效(也就意味着getter/setter不会被继承),这将大大削弱getter/setter的做用。
另外一方面,假如accessor descriptor定义的属性不会被继承,那么data descriptor定义的属性应不该该被继承?若是也不被继承,那么JavaScript还怎么作到面向对象语言最基本的“继承”?若是data descriptor定义的属性可以被继承,那么accessor descriptor与data descriptor的使用场景将出现巨大的割裂,程序员只能经过“属性是否能被继承”来决定是使用accessor descriptor仍是data descriptor,这将大大削弱descriptor的灵活性。
此外,与前面一种状况同理,“模仿传统类继承语言的表现”也是一个重要的缘由。
前面已经对【经过"="操做符为对象添加新属性】的3种状况进行了讨论和解释。接下来咱们看看ECMAScript标准是如何正式地定义"="操做符的行为的。
AssignmentExpression:LeftHandSideExpression=AssignmentExpression表达式在运行时的求值算法:
说明:
abcd步骤,对于赋值表达式的左值取引用(至关于获得变量/属性在内存中的地址),对于右值求值。e步骤是为了处理func = function() {}
这种函数表达式赋值的状况,本文不讨论。f步骤中的PutValue(lref, rval)
才是真正执行赋值操做的算法。PutValue ( V, W )的算法定义:
其中第4步的做用是,对于属性引用V
,获取V
所在的对象(好比对于属性引用a.b.c.prop
,获取到的对象是a.b.c
)。本文讨论的赋值状况会进入第6步的Elseif中。6.a是为了应对true.prop = 2134
这种状况(这是合法的表达式!),不在本文讨论。6.b中的[[Set]]
承担赋值过程的主要操做。[[Set]]
是ECMAScript为对象定义的13个基本内部方法之一,普通对象对这些内部方法的实现算法在这里,特异对象(好比数组)在普通对象的基础上覆盖某些基本内部方法。在这里咱们只看普通对象的[[Set]]
算法:
能够看出,算法在2.b.i步骤作了递归:若是当前对象不存在这个属性,则递归到父对象上找。参数O
随着每次递归而变化,指向当前递归查找到了哪一个对象。而参数Receiver
则不随着递归而改变,始终指向最初被赋值的那个对象。
若是在原型链上找到了同名属性,就会进入OrdinarySetWithOwnDescriptor
的步骤3:
若是在原型链上找不到同名属性,会通过步骤2.c.i,从而最终到达步骤3.e,在目标对象上建立新属性,对应于前面讨论的【若是原型链上不存在同名属性,则直接在obj上建立新属性】状况。
"="操做符赋值是JavaScript中最多见的操做之一,了解它的特殊性有助于更好地利用它、更好地利用“继承”。
除此以外,你会惊讶地发现,Proxy容许咱们拦截的13个对象方法,刚好一一对应于ES标准为对象定义的13个基本内部方法!而Reflect对象中提供的13个方法也与之一一对应!其实Reflect对象提供的13个方法就是普通对象的基本内部方法的简单封装!
如今你应该可以理解为何,在咱们经过Proxy拦截set操做的时候,执行引擎会向咱们暴露出刚刚谈到的receiver
。由于咱们不只仅会拦截到被代理对象(target)的赋值操做,而且,若是代理对象成为其余对象的原型,那么对其余对象(receiver)的赋值也会触发代理对象的set操做。执行引擎会将target和receiver都暴露给咱们,从而咱们能拥有最大的灵活度。
注意,咱们在前面讨论的时候一直强调"="操做符,这是由于,为对象添加、修改属性还有另外一种方法:Object.defineProperty()。这是比"="操做符更增强大、基础的方法,它只对指定的对象进行属性增长、修改,而不会影响到原型链上的对象或被原型链影响。经过它,能够作到"="操做符作不到的事情,好比:为对象设置一个新属性,即便它的原型链上已经有一个non-writable的同名属性。