有这样一个热门问题:javascript
var a = {n: 1}; var b = a; a.x = a = {n: 2}; alert(a.x); // --> undefined alert(b.x); // --> {n: 2}
其实这个问题很好理解,关键要弄清下面两个知识点:java
JS引擎对赋值表达式的处理过程git
赋值运算的右结合性github
形如segmentfault
A = B
的表达式称为赋值表达式。其中A和B又分别能够是表达式。B能够是任意表达式,可是A必须是一个左值。函数
所谓左值,就是能够被赋值的表达式,在ES规范中是用内部类型引用(Reference)描述的。例如:es5
表达式foo.bar
能够做为一个左值,表示对foo这个对象中bar这个名称的引用;spa
变量email
能够做为一个左值,表示对当前执行环境中的环境记录项envRec中email这个名称的引用;code
一样地,函数名func
能够作左值,然而函数调用表达式func(a, b)
不能够。对象
那么JS引擎是怎样计算通常的赋值表达式 A = B
的呢?简单地说,按以下步骤:
计算表达式A,获得一个引用refA
;
计算表达式B,获得一个值valueB
;
将valueB
赋给refA
指向的名称绑定;
返回valueB
。
所谓结合性,是指表达式中同一个运算符出现屡次时,是左边的优先计算仍是右边的优先计算。
赋值表达式是右结合的。这意味着:
A1 = A2 = A3 = A4
等价于
A1 = (A2 = (A3 = A4))
好了,有了上面两部分的知识。下面来看一下JS引擎是怎样运算连等赋值表达式的。
如下面的式子为例:
Exp1 = Exp2 = Exp3 = Exp4
首先根据右结合性,能够转换成
Exp1 = (Exp2 = (Exp3 = Exp4))
而后,咱们已经知道对于单个赋值运算,JS引擎老是先计算左边的操做数,再计算右边的操做数。因此接下来的步骤就是:
计算Exp1,获得Ref1;
计算Exp2,获得Ref2;
计算Exp3,获得Ref3;
计算Exp4,获得Value4。
如今变成了这样的:
Ref1 = (Ref2 = (Ref3 = Value4))
接下来的步骤是:
将Value4赋给Exp3;
将Value4赋给Exp2;
将Value4赋给Exp1;
返回表达式最终的结果Value4。
注意:这几个步骤体现了右结合性。
总结一下就是:
先从左到右解析各个引用,而后计算最右侧的表达式的值,最后把值从右到左赋给各个引用。
如今回到文章开头的问题。
首先前两个var语句执行完后,a
和b
都指向同一个对象{n: 1}
(为方便描述,下面称为对象N1)。而后来看
a.x = a = {n: 2};
根据前面的知识,首先依次计算表达式a.x
和a
,获得两个引用。其中a.x
表示对象N1中的x,而a
至关于envRec.a
,即当前环境记录项中的a。因此此时能够写出以下的形式:
[[N1]].x = [[encRec]].a = {n: 2};
其中,[[]]
表示引用指向的对象。
接下来,将{n: 2}
赋值给[[encRec]].a
,即将{n: 2}
绑定到当前上下文中的名称a
。
接下来,将同一个{n: 2}
赋值给[[N1]].x
,即将{n: 2}
绑定到N1中的名称x
。
因为b
仍然指向N1
,因此此时有
b <=> N1 <=> {n: 1, x: {n: 2}}
而a
被从新赋值了,因此
a <=> {n: 2}
而且
a === b.x
若是你明白了上面全部的内容,应该会明白a.x = a = {n:2};
与b.x = a = {n:2};
是彻底等价的。由于在解析a.x
或b.x
的那个时间点
。a
和b
这两个名称指向同一个对象,就像C++中同一个对象能够有多个引用同样。而在这个时间点
以后,不管是a.x
仍是b.x
,其实早就不存在了,它已经变成了那个内存中的对象.x
了。
最后用一张图表示整个表达式的运算过程: