前言
如果你一看便知这道面试题的答案,恭喜你有着扎实的 JS 功底,可惜我半天都想不明白,则写下这篇记录:
1 | var a = { n: 1 }; |
答案:
1 | console.log(a.x); // undefined |
为什么
先来看两段代码就当热身:
1 | var a = 1; |
1 | var a = { n: 1 }; |
结果似乎不用过脑子,因为日常的编码里已经操练过很多次了。但上面到面试题,却出乎意料的摸不清头绪,你是不是像我一样在面试中思考片秒后,给出这样的答案?
1 | var a = { n: 1 }; |
那我们就逐行分析其中的原由,首先是这两行的简单代码:
1 | var a = { n: 1 }; |
变量 a 在堆内存创建了一个对象 { n: 1 },并指向它。同时变量 b 的引用也指向了 a 创建的那个对象。此时他们的值都为 { n: 1 }。
最磨人的这行代码来了:
1 | a.x = a = { n: 2 }; |
首先根据 js 编译器解析代码原则,会先在作用域中依次寻找 a 和 a.x 是否已经声明过。如果没有则会创建对应的变量。(见:你不知道的 Javascript 上卷[p7])
因为 a 我们已经创建过了,但 a.x 没有,则会在那个创建对象内部声明 x(其值 undefined)
注意一点:此时 a 引用是最开始在堆内存里创建的对象(这理解很重要,会影响能否读懂这题)
当代码被 js 引擎执行起来后,会从右往左依次进行赋值运算,即代码的优先级会变成这样:
1 | a.x = (a = { n: 2 }); |
第一次 a = { n: 2 } 进行赋值:
变量 a 重新在堆内存申请了一个对象地址,其值为 { n: 2 }:
第二次赋值 a.x = a:
的确 a.x 的值是为 {n: 2},但不同的是:这里的 a.x 的 a 是老对象引用(引用并没有被释放掉),a.x 指向了 a 的新地址 {n: 2}:
所以:
1 | console.log(a.x); // undefined |
a.x 为 undefined:因为 a 新的引用地址({n: 2})的对象中没有 x 属性。
a 为 { n: 2 }:因为 a 为新的引用地址。
b 为 { n: 1, x: { n: 2 } }:因为 b 还是指向 a 的旧引用地址,但 a.x 指向了 a 的新引用地址。
最后
似乎豁然开朗,没明白就多看几遍吧。虽然日常不可能写这样会引起 bug 的代码,但的确能测试回答这问题人的水平,反正我面试是挂了,水平像个刚入门的新生一样。
(本篇在循环播放 米津玄师的 Loser 中完成)