本文深入探讨 JavaScript 中连续赋值操作的内部机制,通过 a.x=a={n:2} 等经典案例揭示其执行顺序。文章指出连续赋值并非简单的从右至左拆分,而是涉及引用地址的预取与副本保存,导致属性赋值时指向旧对象。理解这一机制有助于避免潜在的 Bug 和内存管理问题。
引言
前言
本文讨论的连等赋值问题源自国外某 JavaScript 规范,起初并未引起重视,直到近期遇到相关 Bug 才意识到 JS 中连等赋值的特殊性(或称为陷阱)。
经检索发现,网络上有关于连等赋值的精彩讨论(来源1,来源2),其中一个典型案例如下:
var a = {n:1};
a.x = a = {n:2};
console.log(a.x); // 输出?
该代码的输出结果是:
console.log(a.x); // undefined
不知读者是否答对,至少我最初是答错的。
借此机会,我们深入剖析 JavaScript 中连等赋值的底层逻辑。
连续赋值的执行顺序
赋值顺序?
假设存在代码片段 A=B=C;,赋值语句的执行顺序是从右至左,因此核心问题在于:
是猜想1: B = C; A = C; ?
还是猜想2: B = C; A = B; ?
我们知道,若两个变量同时指向同一个对象,对该对象的修改会同步反映在两个变量上,例如:
var a={n:1};
var b=a;
a.n=2;
console.log(b);//Object {n: 2}
利用这一特性,我们可以测试连续赋值的实际顺序。
若按猜想1执行,将 C 替换为具体对象,修改 a 不会同步到 b,因为第一行和第二行分别创建了两个独立的 {n:1} 对象。例如:
var b={n:1};
var a={n:1};
a.n=0;
console.log(b);//Object {n: 1}
若按猜想2执行,将 C 替换为具体对象,修改 a 会同步到 b,因为 a 和 b 同时引用了同一个对象。例如:
var b={n:1};
var a=b;
a.n=0;
console.log(b);//Object {n: 0}
接下来测试真正的连等赋值:
var a,b;
a=b={n:1};
a.n=0;
console.log(b);//Object {n: 0}
结果符合猜想2。若认为此测试不够严谨,可借助 ECMAScript 5 的 setter 和 getter 特性进行验证。
setter 和 getter 作用于变量名而非变量存储的对象,如下示例:
Object.defineProperty(window,"obj",{
get:function(){
console.log("getter!!!");
}
});
var x=obj;
obj;//getter!!! undefined
x;//undefined
可见仅 obj 输出“getter!!!”,而 x 未输出,利用此特性可进一步测试。
连等赋值测试2:
Object.defineProperty(window,"obj",{
get:function(){
console.log("getter!!!");
}
});
a=b=obj;//getter!!! undefined

通过 getter 再次证实,在 A=B=C 中,C 仅被读取一次。
因此,连等赋值的真正运算规则是 B = C; A = B;,即连续赋值从右至左,永远只取等号右侧表达式的结果赋值给左侧。
连续赋值能否拆分执行
连续赋值能拆开写么?
基于上述规则,回归文章开头的案例,若按规则将连续赋值拆分开来书写,结果将不同,例如:
var a={n:1};
a={n:2};
a.x=a;
console.log(a.x);//Object {n: 2, x: Object}
可见,连续赋值语句虽遵循从右至左依次赋值的规则,但仍不可拆分开写,原因在于:
推测:JavaScript 内部为保证赋值语句的正确性,会在一条赋值语句执行前,先取出所有待赋值引用的副本,再依次赋值。
因此,代码 a.x=a={n:2}; 的逻辑可解析为:
1、执行前,先取出 a 和 a.x 中 a 的引用地址副本,此时它们均指向 {n:1}。
2、在内存中创建新对象 {n:2}。
3、执行 a={n:2},将 a 的引用从指向 {n:1} 改为指向新的 {n:2}。
4、执行 a.x=a,此时 a 已指向新对象,而 a.x 因执行前保留了原引用副本,故 a.x 的 a 仍指向原 {n:1} 对象,从而给原对象新增属性 x,内容为 {n:2}(即当前 a 的值)。
5、语句执行结束,原对象由 {n:1} 变为 {n:1,x:{n:2}},而原对象因无人再引用被 GC 回收,当前 a 指向新对象 {n:2}。
6、由此得出文章开头的运行结果,再次执行 a.x 自然返回 undefined。
上述过程按序号图示:


根据上述过程,旧的 a.x 和新的 a 均指向新创建的对象 {n:2},因此它们应当全等。
测试验证:
var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a===b.x); //true

因增加了 var b=a,即原对象多了一条引用,故在第5步时不会被释放,证实了上述结论。
总结与建议
后记
通过了解连续赋值的特点,回过头看文章标题,似乎应改为:
尽量避免使用 JS 的连续赋值操作,除非你完全了解其内部机制及可能产生的后果。