JS语言理解15 连等赋值
面试的时候遇到的一道关于连等赋值的面试题。
题目
在面试的时候遇到这样一道面试题:
1 | var a = {n: 1}; |
不出意外的我答错了,回来之后查阅了一些资料,把自己的理解写出来。
关键原则
首先要理解的关键原则:
(1)赋值表达式
1 | A = B |
上面的表达式就叫做赋值表达式,JS引擎是按照如下步骤计算赋值表达式的:
- 计算表达式A,得到一个引用
refA
- 计算表达式B,得到一个值
valueB
- 将
value
赋给refA
指向的名称绑定 - 返回
valueB
(2)赋值运算右结合,赋值时从右至左永远只取等号右边的表达式结果赋值到等号左侧
1 | let a = b = 1; |
有了上面的规则,JS引擎在计算连等赋值表达式时,例如:
1 | Exp1 = Exp2 = Exp3 = Exp4 |
根据右结合特性,可以转换成:
1 | Exp1 = (Exp2 = (Exp3 = Exp4)) |
拆解为单个的赋值运算,JS引擎总是先计算左边的操作数,再计算右边的操作数,所以接下来的步骤是:
- 计算
Exp1
,得到Ref1
- 计算
Exp2
,得到Ref2
- 计算
Exp3
,得到Ref3
- 计算
Exp4
,得到value4
表达式变成了:
1 | Ref1 = (Ref2 = (Ref3 = Value4)) |
接下来的步骤就变成了:
- 将
Value4
赋值给Exp3
, - 将
Value4
赋值给Exp2
, - 将
Value4
赋值给Exp1
, - 返回表达式最终结果
value4
(3)声明提前
在复制操作之前,JS引擎会首先寻找未声明的变量,并将找到的变量提升到作用于顶部并且声明变量
1 | let a = {}; |
上面赋值的过程是,首先为变量a
在内存中分配一块区域,防止一个空对象{}
,然后将a
的指向这块区域
然后在对a.x
赋值时,由于a.x
并没有声明,所以会首先在那才分配的内存中的空对象中声明x
然后对a.x
赋值,完成赋值操作:
题目分析
根据上面的原则,可以将连续赋值拆开进行
1 | let a = (b = 1); |
首先进行了b = 1
的赋值操作,由于未声明的变量(非严格模式下)会声明为全局变量,所以b
和window.b
都是1
之后又进行了a = b
的操作,此时b
已被赋值为1
,所以a
是1
,window.a
是undeinfed
然后再来拆解之前的面试题,首先声明了变量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.x
是undefined
,要注意的是,由于a
和b
指向同一份内存,所以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
的结果是undefined
,b.x
的结果是{n:2}
,并且b.x === a
的结果是true