本文遵守CC BY-SA 3.0。
前言:
标题真是不知道如何命名,就取最重要的一句话吧。话说最近在看javascript权威指南,感觉对这个语言有种莫名的喜欢。。。这个也应该是一个比较经典的问题了,不是出自此书,据说是jQuery源码中的一个用法,网上找了一阵,stackoverflow上直接是0 results。。可能是问题比较久远,貌似都是10年左右的回答,而且也没有什么让我豁然开朗的解答,倒是在回复中看到了一个思路,于是就顺着想下来了,如果有什么错误,还请斧正。
先把问题放在这里
1 var a = {n:1} 2 var b = a //暂存a 3 a.x = a = {n:2} //问题的源头 4 console.log(a.x) 5 console.log(b.x)
想想看,输出结果都为什么值。
一、梳理
行号1:创建"{n:1}"对象,并将a指向此地址空间(假设为A);
行号2:将b指向a指向的地址空间(还是A);
行号3:
(1) 在赋值没开始的时候:a.x是给地址A中存储的对象分配了一个新属性x,并分配了地址(假设AX);
(2) 开始赋值,由于赋值是右结合运算,所以这句话可以看作a.x = (a = {n:2});
再次分布解析,注意,此时a.x的地址空间是AX,a的地址空间是A,{n:2}被作为新对象被创建,并且分配地址空间(假设B)
1. 其实之所以混乱就是被变量名迷惑了,只要将变量都看作对地址空间或其内部值的操作就简单明了了,比如:
a.x = (a = {n:2})可以看作:
a指向的地址空间被更改成对象{n:2}的地址空间;
a.x中的a的地址空间,即A地址空间存储的x属性(AX地址中的值)被赋值成{n:2};
2. 分析结果
整体赋值完成后,a指向的地址空间更新成C,内部值为{n:2};
还记得b指向的地址空间吗,就是最初的a指向的A,虽然连续赋值语句没有对b进行操作,但是A这块地址空间却被修改了(被赋予新属性x,值为{n:2})
行号4: 输出 a.x 为undefined。
行号5: 输出 b.x 为{n:2}。
二、再努力一次
如果看完梳理仍然觉得懵逼,可以尝试这么想:
可能大家对a = {n:2}这句话没什么问题,实际上,所有皆可看作对象,所有皆可作为引用,于是在连等的语句中,对a.x赋值的时候可以看作是在对A地址空间的x属性进行复制,这个连等操作也实际上是在对各个地址空间的值或属性进行修改,所以,即使我们看到的a被修改了,它最初代表的地址空间没有改变,可以参照下面的例子
现在有三个杯子(A/B/AX),两个标签(a/b/c),一个标记(x1,x2)
1. 向杯子A中放入一号小球(n:1),然后将a/b标签都贴在杯子A上,c标签贴在杯子AX上;
2. 向杯子B中放入二号小球(n:2),
3. 向A中投入一个x1标记,告诉你可以到杯子AX中察看(a.x的创建),AX中投入一个x2标记,告诉你可以到B中找到二号小球(对a.x赋值);
4. 将a标签贴到B杯子上面。
5. 这个时候,看看各个物件的状态,
(1)a标签贴在了B上,b标签还贴在A上,c标签还贴在AX上;
(2)A里面有个1号小球(n:1),一个指向AX的x1标记;
(3)AX里面有个x2标记,指向B;
(4)B里面有个2号小球(n:2);
6. 察看结果:
a.x => B杯子中的x标记(没有,所以undefined)
b.x => A杯子中的x标记(指向杯子AX,再指向杯子B,发现是个2号小球(n:2));
注:可能看到这里你还是有疑问,为什么a标签的转移要放到最后一步,明明是应该在中间步骤啊,其实在处理赋值语句的时候,如果你修改了属性(x),那将对该引用指向的地址空间存储的值进行修改,也就是改值而非改引用(杯子没变),而对a的赋值才是对引用的修改(移动a标签),也就是说,做a的赋值的时候代表着移动a标签,而对a.x的赋值则代表着对A杯子的x标记作处理,可以理解成,做赋值之前,a.x就已经代表了对A杯子的操作(与期间a标签是否移动无关),当然,这种逻辑可能只有在连等的时候才会出现。