前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >连等表达式的核心原理

连等表达式的核心原理

作者头像
用户6901603
发布2020-11-25 11:17:53
4310
发布2020-11-25 11:17:53
举报
文章被收录于专栏:不知非攻不知非攻

有这样一道面试题,在群里引发了剧烈的讨论,讨论一天之后,仍然有同学还存在疑问。

代码语言:javascript
复制
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 的区别是什么?

代码语言:javascript
复制
var a = 3;
var b = a++;
// 此时 b 是多少?a 是多少?为什么
代码语言:javascript
复制
var a = 3;
var b = ++a;
// 此时 b 是多少?a 是多少?为什么

这两个例子的结果是不同的,原因就在于,a++++a 这两个表达式的返回结果不一样。

代码语言:javascript
复制
var a = 2
a++
// 此时 a++ 的返回结果为 2,而不是 3
代码语言:javascript
复制
var a = 2
++a
// 此时 a++ 的返回结果为 3

知道了表达式的返回结果,上面的问题的答案就不言而喻。

此时回到正题。我们知道,在 a.x = a = {n: 2} 这个表达式中,a = {n: 2} 需要先被运算,那么他们其实就等价于

代码语言:javascript
复制
a.x = (a = {n: 2})

第二步,就是把先运算的表达式的返回结果给第二步继续运算。他们的返回结果是什么呢?这里也有一个容易引起歧义的误解。

当我们使用变量声明时,返回值是 undefined

代码语言:javascript
复制
var a = 10
// undefined

但是在概念上一定要明确,变量声明与表达式是有区别的,变量声明的返回值为 undefined,但是表达的返回结果各不一样

例如

代码语言:javascript
复制
20 > 10   // 返回结果 true
!100       // 返回结果 false
a = 20    // 返回结果 20
a = {n: 2}  // 返回结果 {n: 2}

因此,仅仅从运算结果上分析

代码语言:javascript
复制
a.x = a = { n: 2 }

// 等价于
a = {n: 2}
a.x = {n: 2}   // 此时的 {n: 2},是上一个表达式的返回结果

如果只是理解到这里,可能还无法得到正确的答案,甚至会得出错误的答案,因此,还有我们没注意到的小细节。

2

表达式的规则

第二个需要我们用小本本记下来的基础知识,是关于赋值表达式的内部规则。要读懂该规则,就需要大家多一点耐心和搞学术的钻研精神,否则必然会被绕晕。

在 ECMAScript 的标准文档中的第十二章节,专门写明了表达式的规则。其中赋值表达式,的规则如下:

看上去很厉害的样子,就是看着有点晕!

先明确几个关键词的含义。

代码语言:javascript
复制
AssignmentExpression:赋值表达式 
LeftHandSideExpression: 左表达式 
AssignmentOperator:赋值运算符 

图中完整的表达了赋值运算表达式的逻辑处理过程。上部分描述了等号的逻辑,下部分描述了其他赋值运算符的通用逻辑。

文档中详细列出了所有的赋值运算符

这里需要给大家翻译一下,看得懂的,就直接跳过就好。但是不经常阅读文档的人,可能有一些单词可能看不懂,例如 lref,rref 代表什么含义不是很明确。

翻译之前,先把这几个概念明确一下,有助于大家理解。

代码语言:javascript
复制
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.xa 的引用都已经被明确好的,因此,即使在赋值过程中,a = {n: 2}a 的引用发生了变化,但是最初设定的 axref 的引用不会发生改变。

而在我们的例子中,axref 的引用,本质是通过 {n: 1} 的引用去访问 {n: 1} 中的 x。因此在a = {n: 2} 的赋值过程中,虽然变量 a 的引用发生了变化,但是并不会影响 axrefaxref 始终都是通过 {n: 1} 去访问 x。

再来回顾一下我们的例子。

代码语言:javascript
复制
var a = { n: 1 }
var b = a
a.x = a = { n: 2 }
console.log(a.x)  // 打印结果是什么

简单解释就是,先明确 a.xa 的引用,他们的引用变化,只有在自身赋值时才会发生改变。a.x 的引用并不会因为 a = {n: 2} 发生变化。因此,下面的写法与案例是等价的

代码语言:javascript
复制
var a = { n: 1 }
var b = a
b.x = a = { n: 2 }

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 不知非攻 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1
  • 2
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档