Javascript 连续赋值的问题深究

牛客_帮打个广告的前端群里碰到的一个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

执行顺序简单来说是按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,propx,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一下解释器~

仅供参考