最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 连等表达式的核心原理

    正文概述 掘金(这波能反杀)   2020-12-04   425

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

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

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

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

    var a = { n: 1 }
    var b = a
    a.x = a = { n: 2 }
    console.log(a.x)  // 打印结果是什么
    

    简单解释就是,先明确 a.xa 的引用,他们的引用变化,只有在自身赋值时才会发生改变。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介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元