javascript 连等赋值问题

javascriptvar a = {n:1};  
var b = a; // 持有a,以回查  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> {n:2}

请问结果为何是这样?

我的理解是连等赋值从右向左运算的,当a被复制为{n:2}之后,

为什么a.x中的a仍然指向{n:1}?

阅读 31.4k
评论
    17 个回答
    • 1.6k

    同意3楼和4楼同学说的。连等是先确定所有变量的指针,再让指针指向那个赋值({n:3})。

    对于 a.x = a = {n:2},楼主原先的思路应该是:

    1. 先把 {n:2} 赋值给 a
    2. 然后再创建 a.x,将 {n:2} 再赋值给 a.x

    这样似乎确实说不通 a.x 的值是 undefined,因为 a.x 确实是被赋值了的啊。
    可是事实上,a.x 的值却是 undefined。

    再来看一下这个: a = a.x = {n:2}的话,按楼主原先的思路应该是:

    1. 先把 {n:2} 赋值给 a.x,那么也就相当于 b.x = {n:2}
    2. 再把 a 重新指向 {n:2}。那么这是后 a.x 的值确实是 undefined,a 对象 {n:2} 中就没有 x 属性嘛。

    按楼主的思路,上述两种方式的结果应该是不同的。但事实却是a = a.x = {n:2}a.x = a = {n:2}的结果是一致的。所以楼主的那种赋值的思路是不对的。

    事实上,解析器在接受到 a = a.x = {n:2} 这样的语句后,会这样做:

    1. 找到 a 和 a.x 的指针。如果已有指针,那么不改变它。如果没有指针,即那个变量还没被申明,那么就创建它,指向 null。
      a 是有指针的,指向 {n:1};a.x 是没有指针的,所以创建它,指向 null。
    2. 然后把上面找到的指针,都指向最右侧赋的那个值,即 {n:2}

    所以执行以后,就有了如下的变量关系图。楼主可以慢慢体会下,想通了就很简单的。

    变量关系图

      • 11.8k

      赋值是从右到左的,但不要被绕晕了, 其实很简单,从运算符优先级来考虑

      a.x = a = {n:2};
      

      .运算优先于=赋值运算,因此此处赋值可理解为

      1. 声明a对象中的x属性,用于赋值,此时b指向a,同时拥有未赋值的x属性
      2. 对a对象赋值,此时变量名a改变指向到对象{n:2}
      3. 对步骤1中x属性,也即a原指向对象的x属性,也即b指向对象的x属性赋值

      赋值结果:

      a => {n: 2}
      b => {n: 1, x: {n: 2 } }    
      
        • 1.7k
        var a = {n:1}; 
        var b = a; // 持有a,以回查  
        a.x = a = {n:2};  
        alert(a.x);// --> undefined  
        alert(b.x);//

        这个问题如果对js或者说对计算机语言不熟悉很容易陷在里面。

        var a = {n:1}; 
        /*定义a,a赋值为`{n:1}`;
        为a在内存堆中分配一块内存用于存储`{n:1}`,假设其地址为add_1;
        此时add_1引用计数为1,即a,内容为`{n:1}`。*/ 
        var b = a; 
        /*定义b,b赋值a,add_1被b引用。
        此时add_1引用计数为2,即a和b,内容为`{n:1}`。*/  
        a.x = a = {n:2};
        /*(`=`赋值运算符:关联性为从右向左,优先级为3。`.`成员访问运算符:关联性为从左向右,优先级为19。19>3,所以先计算成员访问运算符)
        (1):a.x是成员访问运算表达式,a.x中的x赋值为`a = {n:2}`的返回值`{n:2}`,add_1被改写`{n:1,x:{n:2}}`。
        此时add_1引用计数为2,即a、b,内容为`{n:1,x:{n:2}}`。
        (2):a赋值为`{n:2}`;
        为a在内存堆中分配一块内存用于存储`{n:2}`,假设其地址为add_2;
        此时add_1引用计数为1,即b,内容为`{n:1,x:{n:2}}`。
        此时add_2引用计数为1,即a,内容为`{n:2}`。*/ 
        alert(a.x);
        /*现在a的存储地址add_2,内容为{n:2},上面并不存在a.x属性,所以为undefined*/ 
        alert(b.x);
        /*现在b的存储地址add_1,内容为{n:1,x:{n:2}},所以b.x为{n:2}*/ 
        

        吵来吵去,都在问连续赋值,又看不到计算机内部运行过程,官方文档的定义每个人理解又不同,所以从最简单的最笨的办法出发,a、b、c排序有6中可能,如下:

        a = {n:2};a.x = {n:2};a.x = a;
        a = {n:2};a.x = a;a.x = {n:2};
        a.x = {n:2};a = {n:2};a.x = a;
        a.x = {n:2};a.x = a;a = {n:2};
        a.x = a;a.x = {n:2};a = {n:2};
        a.x = a;a = {n:2};a.x = {n:2};

        经过测试,从a.x = a = {n:2}的结果验证

        a.x = a;
        a.x = {n:2};
        a = {n:2};

        顺序为正解。

        引用地址的事情又是一个话题,此处不再赘述

          • 8.6k
            • 10.6k

            虽然这个问题过去很长时间了,但是今天我来个让所有人一看就懂的回答:

            这个问题最关键的地方就是赋值表达式的右结合性。

            但是,你真正懂得右结合性是怎样起作用的吗?

            看下面的连续赋值表达式:

            exp1 = exp2 = exp3 = ... = expN;

            其中的exp是一个表达式,并且除最后一个expN外,其他表达式都必须可以作为左值

            你能告诉我它是怎样运算吗?

            是这样的,首先根据赋值运算的右结合性,可以改写成:

            exp1 = (exp2 = (exp3 = (... = expN)...);

            然后按照下面步骤进行运算:

            1. 解析exp1;

            2. 解析exp2;

            3. 解析exp3;
              ...

            N. 解析expN;

            以上步骤完成后,上面表达式变成了:

            ref1 = (ref2 = (ref3 = (... = value)...);

            其中value是表达式expN的值。接下来的步骤是:

            1. 将value赋给引用refN-1;

            2. 将value赋给引用refN-2;
              ...

            N-1.将value赋给引用ref1;

            结束。

            现在你应该可以理解为什么

            a.x = a = {n: 2};

            是怎么回事了。关键在这里:

            • a.x是对a这个对象下的x这个名称的引用。

            • a可以看成envRec.a,是对envRec这个对象下的a这个名称的引用。

            envRec是什么呢?是当前执行上下文中的环境记录项,可以认为保存局部变量的那个内部对象。

              • 78
              a.x = a = {n:2};  
              

              理解这个其他的都好理解了,这个赋值是从右边向左边进行的。但是在真正赋值的时候,并不是看上去的那样。真正的赋值表面上和下面的一样:

              a = {n:2}
              a.x = {n:2}
              

              但因为a只是一个变量,在内部的时候其实是这样的:

              a = {n:2} // 正常的变量赋值,将一个值赋给变量a,其实变量a保存的是一个地址,类似指针吧。
              
              // 下面的是这样的,因为涉及到了属性的赋值,所以在编译时期(语法检查什么的),这个a会被替换为其真正
              // 的一个地址
              // 所以下面的可能是 _a = getRefOf(a); _a.x = {a:2} 
              a.x = {a:2} 
              

              然后运行的时候,a地址指向的值增加了一个x属性,由于b引用这个值,因此b可以访问这个x的值。而a这个变量就被赋值给了{n:2}了。

              要知道js在运行前有语法错误检查和一些其他的准备的,不是像我们写的代码看上去那样。也就是说,如果涉及到变量的属性的赋值什么的,都会对原变量保存一个副本,在这个副本上进行操作,来保证属性的确是被附到了原来的对象上。这个我好像在哪个书上看到过,但是记得有些模糊了,可能有错误的地方。

                我理解这涉及两个过程即求值过程和运算过程,求值过程都是从左到右,而运算过程有自己的运算顺序

                a.x = a = {n:2};
                

                这条语句中a.x a 和 {n:2}分别是三个表达式,需要先对他们求值,然后才会进行赋值操作,求值过程是先于赋值过程的

                  • 1.8k

                  问题在于连等语句,是一条语句啊!

                  javascripta.x = a = {n:2};  
                  

                  在程序运行到之后,先确定好了 a.xa 的引用,再从右往左开始赋值的。

                    . 运算优先于 = 赋值运算,且赋值从右到左

                    因此 a.x = a = {n:2} 可以分解为
                    a.x = {n:2}; // b 作为引用,即 b = {n:1, x:{n:2}}
                    a = {n:2}; // a 被重新赋值对象,即 b 不再为引用

                      • 18.7k

                      js按照从左往右的顺序计算表达式
                      计算完表达式后才进行运算操作
                      我们回到a.x = a = {n:2}; 这个赋值语句来看
                      1)首先,js引擎计算表达式的值
                      1.1 对a.x 会在a上添加属性,其中为undefined,注意此时b指向的对象和a指向的对象为同一个,都会有一个属性x,a.x的计算结果为一个值为undefined的指针变量,假设为C
                      1.2 计算a得到为一个添加了属性x的对象
                      2)赋值操作从左往右进行,那么
                      2.1 a={n:2}赋值操作后 a指向{n:2}表达式对象的临时变量。赋值操作返回对象为{n:2}
                      2.2 接下来执行a.x={n:2}的赋值操作,注意同一个表达式,JS不会计算2遍;
                      此时的a.x为1.1计算得到的结果,也即是赋值操作对指针变量C赋值,结果为{n:2}

                        • 2
                        • 新人请关照
                        
                        var a = {n:1};  
                        var b = a;
                        a.x = a = {n:2};  
                        // 由于 .运算优先高于=赋值运算
                        // 第三个步骤可以看成:
                        // a = a.x = {n:2};
                        // 再次拆分一下:
                            // a.x = {n:2}; 引用的关系,此时b为 {n:1,x:{n:2}}
                            // a = {n:2}; a更改了赋值
                        console.log(a.x);// --> undefined  
                        console.log(b.x);// --> {n:2}
                        
                          • 2.8k

                          写了 10 年 Javascript 未必全了解的连续赋值运算

                            支持@towry的说法,预编译时候先把a.x和a的变量提出来,然后再去赋值

                              • -2
                              • 新人请关照

                              个人理解的流程:

                              var _a;
                              var _b;
                              Object.defineProperty(window, 'a', {
                                get (){
                                  debugger
                                  console.log('get a', _a)
                                  return _a
                                },
                                set (val){
                                  debugger
                                  console.log('set a', val)
                                  _a = val
                                }
                              })
                              Object.defineProperty(window, 'b', {
                                get (){
                                  debugger
                                  console.log('get b', _b)
                                  return _b
                                },
                                set (val){
                                  debugger
                                  console.log('set b', val)
                                  _b = val
                                }
                              })
                              
                              test()
                              function test (){
                                debugger
                                var ddd = new Proxy({n:1}, {
                                  get (target,prop){
                                    debugger
                                    var ccc = Reflect.get(...arguments)
                                    console.log('get ddd',prop,ccc)
                                    return ccc
                                  },
                                  set (target, prop, val){
                                    debugger
                                    console.log('set ddd',prop,val)
                                    return Reflect.set(...arguments)
                                  }
                                })
                              
                                var ccc = new Proxy({n:2}, {
                                  get (target,prop){
                                    debugger
                                    var ccc = Reflect.get(...arguments)
                                    console.log('get ccc',prop,ccc)
                                    return ccc
                                  },
                                  set (target, prop, val){
                                    debugger
                                    console.log('set ccc',prop,val)
                                    return Reflect.set(...arguments)
                                  }
                                })
                              
                                a = ddd
                                b = a; // 持有a,以回查  
                                a.x = a = ccc;  
                                console.log(a.x);// --> undefined  
                                console.log(b.x);// --> {n:2}
                                console.log(a,b)
                              }

                                var a = {n:1};
                                var b = a;
                                {n:1}.x = a = {n: 2}
                                这样写,不知道有没有人能够理解

                                clipboard.png
                                取自--《JavaScript高级程序设计》

                                该答案已被忽略,原因:

                                  该答案已被忽略,原因:

                                    撰写回答

                                    登录后参与交流、获取后续更新提醒