在 ECMAScript(3rd) 文档中有关于赋值表达式的解释:
The production AssignmentExpression:
LeftHandSideExpression = AssignmentExpression
is evaluated as follows:
- Evaluate LeftHandSideExpression.
- Evaluate AssignmentExpression.
- Call GetValue( Result( 2 ) ).
- Call PutValue( Result( 1 ), Result( 3 ) ).
- Return Result( 3 ).
小弟自己翻译以下:
- 左边引用
- 右边引用
- 右边计算
- 赋值
- 返回
看上去,好像跟我们从红皮书看的运算顺序不一样,链式赋值都是从右往左的啊,怎么会是左边引用先呢?
根据这个,有比较经典的以下题目,原题:
var a = { n: 1 }, b = a; // ①
a.x = a = { n: 2 }; // ②
console.log(a.x); // undefined
console.log(b.x); // { n: 2 }
对以上标记的语句进行分析:
①
- 声明 a 和 b ,分配新的堆 { n: 1 },a 和 b 都赋值为指向 { n: 1 } 的地址,栈中 a 和 b 是不同的,但指向同一个堆 { n: 1 }
②
- 点运算比赋值运算优先级高,所以 a.x 会先运算,栈中找到 a,指向堆 { n: 1 },找不到 a.x,所以 js 为堆内存的对象加了个新成员 x,赋予初始值 undefined。
这也是为啥引用未定义的变量会报错,而引用一个对象的不存在的属性会返回 undefined。
(右边第一个等号开始)
- 【左边引用】a 此时是指向
{ n: 1 }
的这个堆 - 【右边引用】分配了新的堆内存,存放
{ n: 2 }
- 【右边计算】
{ n: 2 }
- 【赋值】
a = { n: 2 }
,此时这个 a 已经由指向 (1) 那个堆,改为了 (3) 的新堆了,而此时 b 还是指向 (1) 的堆,即a = { n: 2 },b = { n: 1 }
- 【返回】
{ n: 2 }
(右起第二个等号开始)
- 【左边引用】a.x 为 undefined,注意(1) 比 (5) 先运算,所以这个 a 啊,已经引用过了,他不会再因为 (5) 改变,a 其实是 b,即指向
{ n: 1 }
!
增强理解:变量 a 在引用一次以后,他就不再变化了——点运算 a.x 让 a 引用了。而在(5)这个 a 赋值了新堆,那么 a 指了 { n: 2 }
,而 b 的引用还存在,所以 { n: 1 }
没被回收,a.x 也即 b.x
- 【右边引用】这里就是其右边等号的返回值,即(8)的
{ n: 2 }
- 【右边计算】
{ n: 2 }
- 【赋值】
a.x = { n: 2 }
,其实就是 b 的变量赋值! - 【返回】
{ n: 2 }
结果如下:
console.log(a) // { n: 2 }
console.log(b) // { n: 1, x: { n: 2 } }
在so网站上找到一段代码,我们可以修改一下 a 的get/set,看看 ② 到底执行的顺序是啥:
var _a;
var logging = false;
Object.defineProperty(window, 'a', {
get: function () {
if (logging) {
console.log('getting a');
}
return _a;
},
set: function (v) {
if (logging) {
console.log('setting a');
}
_a = v;
}
});
a = { n: 1 };
var b = a;
logging = true;
a.x = a = { n: 2 };
logging = false;
console.log(a);
console.log(b);
可以看控制台的输出,是先运算了 getting
再运算 setting
,而且只运算了一遍。也就是说,a 只是被引用了一遍,而且还是在赋值以前,那就是 a.x。
getting a
setting a
{ n: 2 }
{ n: 1, x: { n: 2 } }
结尾:其实有几个点还是没弄清楚
- 右边的引用和计算到底分别干了啥?以上没分析到答案
- 变量未声明直接使用会报错,对象的未声明属性却不会
console.log(hah) // Uncaught ReferenceError: hah is not defined
var o = { o: 'o' }
console.log(o.hah) // undefined
console.log(o) // { o: 'o' } 这个也没看出来有给初始值呢,也许,是在引用到未声明的属性时才会分配初始值,没有引用到就不会分配,直接将原对象引用出来
参考文章
1. What's the result of this JavaScript code snippet, and why?
2. JS:连续赋值运算