有这样一道面试题,在群里引发了剧烈的讨论,讨论一天之后,仍然有同学还存在疑问。
var a = { n: 1 }
var b = a
a.x = a = { n: 2 }
console.log(a.x) // 打印结果是什么
这个问题其实在网络上也非常火,但是,正确的解读却非常少。许多人虽然最终给出了正确的结论,但是解释的原因却存在问题。
正确理解这道题,首先得补习几个前置的基础知识,这几个基础知识,大家应该拿小本本记下来,因为,掌握它们的人,少之又少。
1
运算符的优先级与结合方式
给大家分享一个表格。
优先级 | 运算符 | 功能 | 结合方式 | 1 | () [] . | 括号、数组、成员访问 | 从左向右 | 2 | ! ~ ++ -- + - | 否定、按位否定、递增、递减、正负号 | 从右向左 | 3 | * / % | 乘、除、取模 | 从左向右 | 4 | + - | 加 减 | 从左向右 | 5 | << >> | 左移、右移 | 从左向右 | 6 | < <= >= > | 小于、小于等于、大于等于、大于 | 从左向右 | 7 | == != | 等于 不等于 | 从左向右 | 8 | & | 按位于 | 从左向右 | 9 | ^ | 按位异或 | 从左向右 | 10 | 竖 | 按位或 | 从左向右 | 11 | && | 逻辑与 | 从左向右 | 12 | 双竖线 | 逻辑或 | 从左向右 | 13 | = += -= *= ... | 各种赋值方式 | 从右向左 |
---|
这张表格关键因素有三个,一个是如何解读优先级,二是如何理解结合方式,三是关注表达式的返回结果
一、正确解读优先级
本来优先级在这里是非常明确的,之所以成为关键因素,是因为许多人为了强行解释,把优先级的因素在此题中作了过度解读。
这里涉及到两个运算符,.
与 =
。.
作为最高优先的存在,此处仅仅只是把 a.x
看成一个整体,而不会有后续的运算。有的人认为这里还会因为 a.x
的优先级更高,所以还应该给其赋值一个 undefined
。这样理解行不行?肯定不行!
此时 a.x
已经处于一个赋值表达式中,a.x = undefined
又是另外一个新的赋值表达式,属于无中生有。
二、正确解读结合方式
上图中,大多数运算符的结合方式,都是从左向右。但是有两个特殊的,是从右向左。这两个特殊的点,常常喜欢被作为考核题目。而刚好,这个题中,就需要考核赋值运算符 =
的结合方式
从右向左,也就意味着,在 a.x = a = {n: 2}
中,要先计算 a = {n: 2}
三、关注表达式的返回结果
表达式的返回结果是很多人忽略的一个重点。容易犯错,所以就容易作为考核点。例如面试的时候,喜欢问 a++
与 ++a
的区别是什么?
var a = 3;
var b = a++;
// 此时 b 是多少?a 是多少?为什么
var a = 3;
var b = ++a;
// 此时 b 是多少?a 是多少?为什么
这两个例子的结果是不同的,原因就在于,a++
与 ++a
这两个表达式的返回结果不一样。
var a = 2
a++
// 此时 a++ 的返回结果为 2,而不是 3
var a = 2
++a
// 此时 a++ 的返回结果为 3
知道了表达式的返回结果,上面的问题的答案就不言而喻。
此时回到正题。我们知道,在 a.x = a = {n: 2}
这个表达式中,a = {n: 2}
需要先被运算,那么他们其实就等价于
a.x = (a = {n: 2})
第二步,就是把先运算的表达式的返回结果给第二步继续运算。他们的返回结果是什么呢?这里也有一个容易引起歧义的误解。
当我们使用变量声明时,返回值是 undefined
var a = 10
// undefined
但是在概念上一定要明确,变量声明与表达式是有区别的,变量声明的返回值为 undefined
,但是表达的返回结果各不一样
例如
20 > 10 // 返回结果 true
!100 // 返回结果 false
a = 20 // 返回结果 20
a = {n: 2} // 返回结果 {n: 2}
因此,仅仅从运算结果上分析
a.x = a = { n: 2 }
// 等价于
a = {n: 2}
a.x = {n: 2} // 此时的 {n: 2},是上一个表达式的返回结果
如果只是理解到这里,可能还无法得到正确的答案,甚至会得出错误的答案。
还有我们没注意到的小细节,我们继续。
2
表达式的规则
第二个需要我们用小本本记下来的基础知识,是关于赋值表达式的内部规则。要读懂该规则,就需要大家多一点耐心和搞学术的钻研精神,否则必然会被绕晕。
在 ECMAScript 的标准文档中的第十二章节,专门写明了表达式的规则。其中赋值表达式,的规则如下:
看上去很厉害的样子,就是看着有点晕!
先明确几个关键词的含义。
AssignmentExpression:赋值表达式
LeftHandSideExpression: 左表达式
AssignmentOperator:赋值运算符
图中完整的表达了赋值运算表达式的逻辑处理过程。上部分描述了等号的逻辑,下部分描述了其他赋值运算符的通用逻辑。
文档中详细列出了所有的赋值运算符
这里需要给大家翻译一下,看得懂的,就直接跳过就好。但是不经常阅读文档的人,可能有一些单词可能看不懂,例如 lref,rref 代表什么含义不是很明确。
翻译之前,先把这几个概念明确一下,有助于大家理解。
lref:left reference 左引用
lval:left value 左值
rref:right reference 右引用
rval:right value 右值
第一种情况,对于赋值运算符 =
来说,内部逻辑步骤如下:
1、先判断左表达式的类型,如果不是 ObjectLiteral/ArrayLiteral「Yield、Await」,就先让左表达式的结果为 lref。然后调用 ReturnIfAbrupt
方法判断左引用的类型,可能是一个标识符,可能是一个对象访问 a.x
等,甚至可能是 undefined
,如果左表达式是一个标识符引用,并且右侧是一个匿名函数,则直接设定左引用的值为 rval:此时为一个函数。
2、如果表达式不是函数,让表达式的结果为 rref。然后通过 GetValue(rref) 得到 rval。
3、然后通过 PutValue(lref, rval) ,指定左引用的值为右值。
4、最后返回右值 rval。
第二种情况,对于其他的赋值运算符来说,内部逻辑如下:
1、Let lref be the result of evaluating LeftHandSideExpression. 明确左表达式的结果为 lref
2、Let lval be ? GetValue(lref). 将 lref 作为参数传入 GetValue ,计算 lval 的值。
3、Let rref be the result of evaluating AssignmentExpression. 明确赋值表达式的结果为 rref
4、Let rval be ? GetValue(rref). 将 rref 作为参数传入 GetValue,计算 rval 的值。
5、到这里就很简单了,明确具体的赋值运算符是什么,使用 op 确认
6、将右值赋值给左值, lval op rval, 并且使用一个变量 r 来接收运算结果
7、使用 PutValue(lref, r).
将 r 设定给左引用
8、最后返回 r
翻译之后,可能还是有点难懂,用通俗一点的表达来描述
对于 a = b
这样的等号赋值表达式来说,经历的逻辑步骤大概如下:
1、先明确 a 的引用 lref
2、再明确 b 的引用 rref
3、调用内部方法 GetValue(rref) 得到 b 的值 rval
4、通过调用 PutValue(lref, rval) 把 b 的值设置给 a 的引用 lref
5、返回 b 的值 rval
对于 a += b
这样的赋值表达式来说,经历的逻辑步骤大概如下
1、先明确 a 的引用 lref
2、调用内部方法 GetValue(lref) 得到 a 的值 lval
3、再明确 b 的引用 rref
4、调用内部方法 GetValue(rref) 得到 b 的值 rval
5、执行运算符逻辑,lval += rval,设定一个内部变量 r ,接收运算结果
6、调用内部方法 PutValue(lref, r),a 的引用 lref 指向 r
7、返回 r
我们可以得出结论,在赋值运算符中,第一件要做的事情,就是先要明确左边表达式的引用。
在 a.x = a = {n: 1}
的运算过程中
1、我们要首先明确左表达式 a.x
的引用,我们设定为 axref,注意,此时 axref 的引用已经被确定好了,就是通过 {n: 1}
去访问 x
,这是关键
2、其次我们要明确右表达式的引用,设定为 rref
3、然后我们要明确 右边表达式的值 rval,可是右表达式又是一个完整的赋值表达式 a = {n: 2}
,于是此时自然需要进入一个递归逻辑,先明确好这个表达式中的具体情况,得到这个表达的最终返回结果,就是 rval 的值
4、明确 a = {n: 2}
中,左表达式的引用,设定为 aref
5、明确 a = {n: 2}
中,右表达式的引用和值,因为直接是一个结果,我们就不做更多分析,右边的值,就直接是 {n: 2}
6、明确 a = {n: 2}
中,左引用对应的值,通过调用内部方法 PutValue(aref, {n: 2})
,此时,a 的引用 aref 被更改,注意,这里无法影响到 axref,这是核心关键。
7、明确 a = {n: 2}
的返回值为 {n: 2}
8、得到右表达式的值 rval 为 a = {n: 2}
的返回值:{n: 2}
,就可以调用内部方法,设置左引用的值 PutValue(axref, {n: 2})
,此时 axref 的引用才发生了变化
9、最后返回 {n: 2}
是不是有点被绕晕了。不过没关系,此时我们需要关注的重点是,这整个过程中,在所有的赋值之前,a.x
与 a
的引用都已经被明确好的,因此,即使在赋值过程中,a = {n: 2}
让 a
的引用发生了变化,但是最初设定的 axref
的引用不会发生改变。
而在我们的例子中,axref
的引用,本质是通过 {n: 1}
的引用去访问 {n: 1}
中的 x。因此在a = {n: 2}
的赋值过程中,虽然变量 a 的引用发生了变化,但是并不会影响 axref
。 axref
始终都是通过 {n: 1}
去访问 x。
再来回顾一下我们的例子。
var a = { n: 1 }
var b = a
a.x = a = { n: 2 }
console.log(a.x) // 打印结果是什么
简单解释就是,先明确 a.x
与 a
的引用,他们的引用变化,只有在自身赋值时才会发生改变。a.x
的引用并不会因为 a = {n: 2}
发生变化。因此,下面的写法与案例是等价的
var a = { n: 1 }
var b = a
b.x = a = { n: 2 }
console.log(a.x)
3
最后
如果你想要更多了解我,欢迎关注我的公众号 不知非攻。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!