JS语言理解15 连等赋值

面试的时候遇到的一道关于连等赋值的面试题。

题目

在面试的时候遇到这样一道面试题:

1
2
3
4
5
6
7
8
var a = {n: 1};

var b = a;

a.x = a = {n: 2};

console.log(a.x);
console.log(b.x);

不出意外的我答错了,回来之后查阅了一些资料,把自己的理解写出来。

关键原则

首先要理解的关键原则:

(1)赋值表达式

1
A = B

上面的表达式就叫做赋值表达式,JS引擎是按照如下步骤计算赋值表达式的:

  1. 计算表达式A,得到一个引用refA
  2. 计算表达式B,得到一个值valueB
  3. value赋给refA指向的名称绑定
  4. 返回valueB

(2)赋值运算右结合,赋值时从右至左永远只取等号右边的表达式结果赋值到等号左侧

1
2
3
4
5
6
7
let a = b = 1;

console.log(a);
console.log(window.a);

console.log(b)
consoel.log(window.b);

有了上面的规则,JS引擎在计算连等赋值表达式时,例如:

1
Exp1 = Exp2 = Exp3 = Exp4

根据右结合特性,可以转换成:

1
Exp1 = (Exp2 = (Exp3 = Exp4))

拆解为单个的赋值运算,JS引擎总是先计算左边的操作数,再计算右边的操作数,所以接下来的步骤是:

  1. 计算Exp1,得到Ref1
  2. 计算Exp2,得到Ref2
  3. 计算Exp3,得到Ref3
  4. 计算Exp4,得到value4

表达式变成了:

1
Ref1 = (Ref2 = (Ref3 = Value4))

接下来的步骤就变成了:

  1. Value4赋值给Exp3
  2. Value4赋值给Exp2
  3. Value4赋值给Exp1
  4. 返回表达式最终结果value4

(3)声明提前

在复制操作之前,JS引擎会首先寻找未声明的变量,并将找到的变量提升到作用于顶部并且声明变量

1
2
let a = {};
a.x = 1;

上面赋值的过程是,首先为变量a在内存中分配一块区域,防止一个空对象{},然后将a的指向这块区域

然后在对a.x赋值时,由于a.x并没有声明,所以会首先在那才分配的内存中的空对象中声明x

然后对a.x赋值,完成赋值操作:

题目分析

根据上面的原则,可以将连续赋值拆开进行

1
let a = (b = 1);

首先进行了b = 1的赋值操作,由于未声明的变量(非严格模式下)会声明为全局变量,所以bwindow.b都是1

之后又进行了a = b的操作,此时b已被赋值为1,所以a1window.aundeinfed

然后再来拆解之前的面试题,首先声明了变量a并且在内存中分配了{n: 1},将a指向了这块内存:

然后声明了变量b,将a赋值给b,也就是将b指向了同一块内存:

然后到了关键节点:

1
a.x = a = {n: 2};

容易产生的误区是上来就按照原则二,直接拆解,先完成a = {n: 2},然后对a.x赋值,得到结果自然是a.x就是{n: 2}

实际上JS引擎会首先对代码进行预处理,发现a.x是一个没有声明的变量(属性),所以会先在{n: 1}内存中声明x这个属性,等待赋值

此时a.xundefined,要注意的是,由于ab指向同一份内存,所以b.x也是undefined

然后按照右结合的原则进行处理,a = {n: 2}a指向了一块新的内存,这是{n: 1}a就没有关系了,但是它不会被GC回收,因为b仍然指向它:

然后执行的等式最左侧,等号右侧的结果是a也就是{n: 2},等号左侧是a.x,但是由于等号左侧的a已经指向了新的内存地址,并没有a.x,但是b.x仍然处于等待被赋值的状态,所以b.x就被赋值{n: 2}。(或者可以理解为当a.x处于被等待赋值状态时,a的指向改变不会影响x属性的复制结果,因为它已经被分配了内存空间)

所以a.x的结果是undefinedb.x的结果是{n:2},并且b.x === a的结果是true

参考