一道被无数人无数次地解释过的经典面试题:
var a = { n:1 };
a.x = a = { n:2 };
console.log(a.x);
理解这道题的第一步需要搞懂这行代码:
var x = y = 100;
相关内容在上一篇《关于 var x = y = 100 你真的会用吗?(上)》中已经介绍过了。
仔细看题目中的第二行,对比我们上面的代码,只是少了 var 关键字,如果把我们上面的代码也去掉 var 关键字,那么就变成:
x = y = 100;
少了 var 关键字后,其中的 x 就变成了一个表达式,之所以这样,是因为 var/let/const 后跟随的一定是变量名,这样变量声明语句才是成立的。不至于与赋值行为混淆在一起。
因此,在 var 声明的语法中,变量名就不能写成 a.x 的样子。
再把两段核心代码放到一起分析下:
var x = y = 100; // 语句1
a.x = a = { n:2 }; // 语句2
结合上面的知识,我们初步可以分析出:
- x 是一个标识符,而 y 和 100 都是表达式,且 y = 100 是一个赋值表达式。
- a.x 是一个表达式,而 a = {n:2} 也是表达式,并且与上一条同理,后者的每一项也都是表达式。
如果上面可以理解,就接着向下分析:
- 在语句1中,并不存在连续的赋值运算,因为 var 从来都不进行计算求值,所以 var x = … 是值绑定操作,而不是将 … 赋值給 x。那么此语句中的唯一赋值运算,就是 y = 100。
- 再看语句2,实际上是两个连续的赋值表达式,按照上面的理解,计算求值是从左到右的,那么a.x是最先被计算求值的,正如刚分析的那样 a.x 是一个表达式,得到的结果是一个引用,这个表达式计算过程也是从左到右的,那么就分解出了 “a” 和 “.x”。
分步解析 a.x 表达式语义:
- 计算表达式左侧 a,得到 a 的引用;
- 将右侧的名字 x 理解为一个标识符,与 “.” 进行运算;
- 计算 a.x 表达式的结果。
此时,我们得到了 a 的引用,那么带着如下几个问题再去从题目中找找答案。
- a 是什么呢?
- a.x 表达式发生了什么?
- a.x 输出什么?
再看一下完整的代码:
var a = { n:1 };
a.x = a = { n:2 };
console.log(a.x);
- 第一行代码中,声明了变量 a,并绑定了初始值 {n: 1},注意这里,严格意义上说是绑定值而不是赋值;
- 紧接着第二行代码中的第一个表达式计算结束后,访问了 a 下面的 x 属性,那么这里就会暂存了 a 的引用,此时 a 为 {n: 1};
- 接下来进行第二行代码的第二个赋值运算 a = {n: 2};
- 此时左侧操作数 a 与上一次暂存的值是相同的;
- 但是接下来的 “=”,使之发生赋值操作,左侧操作数 a 作为一个引用被覆盖了,这个引用仍然是当前上下文中的那个变量 a;
- 记得我们之前暂存的 a 的引用么,这个暂存的 a 并不会随之更新,因为 a = {n: 2} 是一个运算结果,这个结果有且只有引擎知道;
- 接下来代码就变成了这样一个赋值语句,等号左侧是"已经求过值的 a.x",且这个 a 的引用在之前已经暂存过了 (指向的是 {n: 1} );
- a.x 这个被赋值的引用其实是一个未创建的属性,赋值操作将使得原始的 a(之前被暂存的)拥有一个新属性 x;
- 不过因为第二个赋值表达式的原因,原始的 a 已经被新产生的 a 覆盖了,新的值为 {n: 2};
- 那么我们之前暂存的原始 a 就在引用传递过程中丢失了,同理 a.x 也被丢弃,变得毫无意义;
- a.x 变得没有意义,那么对 a.x 的赋值操作也是无意义的。
通过分析可以得出结论,打印 a.x 时,虽然此时的 a 是存在的,但是这个新产生的 a 内部并没有一个 x 属性,因此输出 undefined。