连等表达式的核心原理

简介: 连等表达式的核心原理

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

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 的标准文档中的第十二章节,专门写明了表达式的规则。其中赋值表达式,的规则如下:

image.png

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


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

AssignmentExpression:赋值表达式 
LeftHandSideExpression: 左表达式 
AssignmentOperator:赋值运算符

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


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

image.png

这里需要给大家翻译一下,看得懂的,就直接跳过就好。但是不经常阅读文档的人,可能有一些单词可能看不懂,例如 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 }
相关文章
|
6月前
|
缓存 安全 Java
|
3月前
|
Java 开发者
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案。本文通过技术综述及示例代码,剖析两者在性能上的差异。if-else具有短路特性,但条件增多时JVM会优化提升性能;switch则利用跳转表机制,在处理大量固定选项时表现出色。通过实验对比可见,switch在重复case值处理上通常更快。尽管如此,选择时还需兼顾代码的可读性和维护性。理解这些细节有助于开发者编写出既高效又优雅的Java代码。
47 2
|
3月前
|
存储 分布式计算 监控
|
4月前
ES6 扩展运算符 ...【详解】(含使用场景、实战技巧和范例、实现原理、错误用法)
ES6 扩展运算符 ...【详解】(含使用场景、实战技巧和范例、实现原理、错误用法)
47 5
|
4月前
|
Java
代码优化设计问题之推荐使用函数式方法进行null判断问题如何解决
代码优化设计问题之推荐使用函数式方法进行null判断问题如何解决
|
5月前
|
C语言 C++ 容器
c++primer plus 6 读书笔记 第五章 循环和关系表达式
c++primer plus 6 读书笔记 第五章 循环和关系表达式
|
5月前
|
开发框架 .NET 程序员
掌握C#语言的精髓:基础知识与实用技能详解(数据类型与变量+ 条件与循环+函数与模块+LINQ+异常+OOP)
掌握C#语言的精髓:基础知识与实用技能详解(数据类型与变量+ 条件与循环+函数与模块+LINQ+异常+OOP)
33 0
|
6月前
|
C++
关系表达式:编程中的比较利器
在编程中,关系表达式扮演着至关重要的角色。它们允许我们比较两个或多个值,并基于这些比较的结果来执行相应的操作。关系表达式通过返回布尔值(真或假)来告诉我们两个值之间的关系,从而帮助我们在程序中做出决策。
51 0
|
6月前
|
Java
基本概念【算术、 关系、逻辑、位、字符串、条件、优先级等运算符】(三)-全面详解(学习总结---从入门到深化)
基本概念【算术、 关系、逻辑、位、字符串、条件、优先级等运算符】(三)-全面详解(学习总结---从入门到深化)
69 0
|
人工智能 大数据 程序员
一文看懂开源图化框架中的循环设计逻辑!
相信大家在日常工作中,已经精通各种循环逻辑的实现。就拿我来说吧,多年的工作经验,已经让我可以熟练的使用 C++,Python,英语等多种语言,循环多次输出“hello word”。不过大家有没有想过一个这样的问题:如何在一个有向无环图(Directed Acyclic Graph,简称dag)中实现循环呢?
736 0
一文看懂开源图化框架中的循环设计逻辑!