在牛客_帮打个广告的前端群里碰到的一个js的问题,网上查了一下并没有令人信服的解释,遂深究一下
问题
var a = {n: 1}
var b = a;
a.x = a = {n: 2}
console.log(a.x);
console.log(b.x);
这个代码最后的结果是啥?
执行以下,结果为
a.x = undefined
b.x = {n :2}
简化问题
这个问题在 SegmentFault 里面有解释
问题最重要的是 a.x = a = {n:2}到底干了什么,以及很多人纠结他的执行顺序
我们把问题简化为,搞清楚这段代码的行为
var a = {n: 1}
a.x = a = {n: 2}
工具
我们使用已经有的工具:
执行顺序
先在AST生成器中生成以上代码的抽象语法树
执行顺序简单来说是按AST后序遍历
关键语句分析
a.x = a = {n: 2}
a.x
先执行a.x
,从scope(上下文)中取出a
对象
Function: Interpreter.prototype.getValueFromScope
并且创建一个名为x
的原生对象
Function: Interpreter.prototype.createPrimitive
但这一步并不是把x
作为key绑定到a
对象上,而是暂且把它放到执行堆栈上:
// https://github.com/NeilFraser/JS-Interpreter/blob/master/interpreter.js#L2146
this.stateStack[0].value = [state.object, state.value];
以上object即a
,value即x
a = {n:2}
根据后序遍历的原则,我们从AST上可以清楚的看到,a = {n:2}是先执行的
那么就改变了scope上a
的值,也即是改变了a的指针
那么这时候a.x
中的a仍然是{n:1}
,并且已经脱离了scope,没有其他的变量持有旧的a
的引用,即从程序中无法访问到该对象
那么下次触发GC的时候,这个对象由于没有被引用到,就会被回收,这是后话
a.x = {n:2}
最后执行的是a.x = {n:2}
的赋值操作
由于a
是{n:1}
,无法访问到,这步操作会执行,但是并不会到scope上的a
(指向{n:1}
)产生任何影响
解释器在这时候才会真正将x
作为Key绑定到a对象上,然后赋值{n:2}
具体代码跟踪:
Function: Interpreter.prototype.setValue
由于之前提到的this.stateStack[0].value = [state.object, state.value]
,所以left
是一个数组
进入到第一个if
逻辑中
this.setProperty(obj, prop, value);
Function: Interpreter.prototype.setProperty
其中obj就是旧的a
,prop
是x
,value
是{n:2}
最后在1421行obj.properties[name] = value;
完成赋值操作
但是因为这个a
对象已经没法访问到,所以并没有什么乱用
显然
如果加一句
var b = a;
那么a.x
赋值完成后,由于b
持有旧的a
的引用,那么自然能够通过b
访问到之前的a
即
b = {n:1,x:{n:2}}
可以看下面简化的关于引用的例子:
var a = {n:1};
var b = a;
a = {n:2}
// b仍然指向之前a的对象{n:1},即持有旧的`a`的指针
假设我们做如下改变:
a = a.x = {n: 2};
这样的话,按之前的推断
先执行a.x = {n:2}
,再执行a = {n:2}
显然结果也是相同的
结论
在这个语句的分析中,重点是
语句的执行顺序:按AST后序遍历
a.x
不直接赋值,而是只创建x
原生对象,最后才赋值
所有可以访问到的对象是与执行的上下文绑定的,即scope
访问不到不代表没有执行
其他的进一步的研究大家可以再debug一下解释器~
仅供参考