Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 53 题:输出以下代码的执行结果并解释为什么 #93

Open
zeroone001 opened this issue Apr 11, 2019 · 33 comments
Open

第 53 题:输出以下代码的执行结果并解释为什么 #93

zeroone001 opened this issue Apr 11, 2019 · 33 comments
Labels

Comments

@zeroone001
Copy link

zeroone001 commented Apr 11, 2019

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)

这是连续赋值的坑

@Chorer
Copy link

Chorer commented Apr 12, 2019

结果:
undefined
{n:2}

首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。
后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。


上面是之前写的解释,最近看周爱民老师的文章的时候,发觉这部分解释有不少地方没说到本质上,有的还是错误的,所以我重新结合老师的文章研究了一下,修改如下:
以这段代码为例:

var a = {n:1};
a.x = a ={n:2};
console.log(a.x);  
代码 注释 补充
a 计算单值表达式 a,得到 a 的引用 这里的 a 是初始 a
a.x 将 x 这个标识符作为. 运算符的右操作数,计算表达式 a.x,得到结果值(Result),它是一个 a.x 的“引用” 这个“引用”当作一个数据结构,通常有 base、name、strict 三个成员。无论x 属性是否存在(这里暂时不存在),a.x 都会被表达为 {"base": a, "name": "x", ...}。而这里的 a 仍然指向旧对象。
a 计算单值表达式 a,得到 a 的引用 这里的 a 是初始 a
a = {n:2} 赋值操作使得左操作数 a 作为一个引用被覆盖,同时操作完成后返回右操作数 {n:2} 这里的这个 a 的的确确被覆盖了,这意味着往后通过 a 访问到的只能是新对象。但是,有一个 a 是不会变的,那就是被 a.x 的 Result 保存下来的引用 a,它作为一个当时既存的、不会再改变的结果,仍然指向旧对象。
a.x = {n:2} 指向旧对象的 a 新建了 x 属性,这个属性关联对象 {n:2} 注意,这里对 a.x 进行了写操作(赋值),直到这次赋值发生的那一刻,才有了为旧对象动态创建 x 属性这个过程。

所以,旧对象(丧失了引用的最初对象)和新对象(往后通过 a 可以访问到的那个对象)分别变成:

// 旧对象
a:{
    n:1,
    x:{n:2}
}
// 新对象
a:{
    n:2
}

现在,执行 console.log(a.x),这里 a.x 被作为 rhs(右手端) 读取,引擎会开始检索是否真的有 a["x"] 这个东西,因为此时通过 a 能访问到的只能是新对象,它自然是没有 x 属性的,所以打印 undefined。而且 —— 直到这次读取发生的那一刻,才有了为新对象动态创建 x 属性这个过程。

Note:也就是说,在引擎从左到右计算表达式的过程中,尽管可能遇见类似 a.x 这样本不存在的属性,但无论如何,都会存在 {"base": a, "name": "x", ...} 这样的数据结构,而在后续真正对 x 进行 读写 的时候,这个 x 才会得到创建。

这个代码块所做的事情,实际上是向旧有对象添加一个指向新对象的属性,并且如果我们想要在后续仍然持有对旧对象的访问,可以在赋值覆盖之前新建一个指向旧对象的变量。

@jsonz1993
Copy link

undefined
{n: 2}
具体答案分析和扩展之前写过一篇类似的
https://juejin.im/post/5b605473e51d45191a0d81d8

@bran-nie
Copy link

把 a.x = a = {n: 2}, 换成 b.x = a = {n: 2} 的时候,是不是会好理解了,虽然确实是这样。

@YouziXR
Copy link

YouziXR commented Apr 12, 2019

以前有做过一个一样的题,等号运算符和.运算符优先级的问题。

https://youzixr.github.io/2019/03/05/JS-%E5%88%B7%E9%A2%98%E8%AE%B0%E5%BD%95/

@kailiu999
Copy link

运算符优先级还真没注意,涨姿势了

@WQHASH
Copy link

WQHASH commented Apr 12, 2019

答案如上
注意点:
1: 点的优先级大于等号的优先级
2: 对象以指针的形式进行存储,每个新对象都是一份新的存储地址

@JonathonChen
Copy link

var a = {n: 1}; // a保持对{n:1}对象的引用
var b = a; // b保持对{n:1}对象的引用
a.x = a = {n: 2}; // a的引用被改变

a.x 	// --> undefined
b.x 	// --> {n: 2}

1、.运算符优先,a.x此时保持对{n: 1}的引用,也就是b也保持对{n: 1}的引用,于是{n: 1} => {n: 1, x: undefined},此时a和b还是对原来对象的引用,只不过原来对象增加了x属性
2、=从右往左,a = {n: 2},此时a的引用已经变成了{n: 2}这个对象
3、a.x=a,此时a.x是保持对{ n: 1, x: undefined}中的x引用,也就是b.x,于是{ n: 1, x: undefined} => {n: 1, x: { n: 2}},即b.x = { n: 2 }

@Ray-56
Copy link

Ray-56 commented Apr 12, 2019

@onloner2012
Copy link

为啥不会重新解析a啊

@Zyingying
Copy link

image

image

@ghost
Copy link

ghost commented Apr 12, 2019

有一个网站可以将 JavaScript 代码的执行过程,用可视化的方式呈现出现。具体链接如下:tylermcginnis

从可视化的执行过程来看,并没有之前上面答案所说的对象增加 x 属性的这个过程,也即 { n: 1 } => { n: 1, x: undefined },而是最后直接变成 { n: 1, x: { n: 2 } }

const a = {};
const b = 1;
a.x = b;

第三行代码 a.x = b; 在执行的过程中,会执行一次左查询以及一次右查询。这里所说的“左” / “右”是把 = 操作符作为参照物,a.x 执行左查询是为了弄清楚在 a 对象上是否存在 x 属性,如果存在,那么 a.x = b; 语句执行的是更新属性的操作;反之,则是新增属性的操作。如果在查询的过程中,发现 a 不存在,则引擎会报错。b 执行右查询是为了获取 b 的值。如果在查询的过程中,发现 b 不存在,引擎可能报错,也可能不报错。至于在赋值的过程中,是否执行左查询或者右查询,关键是看 = 的左右两边是否存在变量

这个问题考察的知识点主要有以下这些:

  • . 的优先级高于 = 的优先级
  • = 具有右结合性(执行的方向是从右往左,先执行 = 右边的表达式,然后把结果赋值给 = 左边的表达式,从这里可以得出 = 属于二元操作符),多个 = 的执行过程,可以类比成"递归"的过程
let a = { n: 1 };
const b = a;

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

执行完第一行以及第二行代码之后,变量 a 和 常量 b 指向同一块内存地址(对象 { n: 1 } 在内存里面的内存地址)。换句话说,a 现在是 b 的别名;反之亦然

在执行第三行代码之前,你要知道 a.x = a = { n: 2 } 里面包含两种操作符(.=)。也正是由于 . 的优先级高于 = 的优先级,所以会首先执行 a.x。不过在执行 a.x 的过程中,会执行一次“左”查询。经过左查询之后,发现对象 a 没有 x 属性(在这里你可以认为代码已经变成 ({ n: 1 }).x 或者 b.x),然后会再去执行第一个 = 操作符。由于 = 具有右结合性,所以会先去执行 a = { n: 2 }。在执行的过程中,发现 a = { n: 2 } 是一个普通的赋值操作。而且也正是因为 = 右边是一个对象字面量,所以在这里是不存在右查询以及表达式的计算过程。不过在把 { n: 2 } 赋给变量 a 之前,需要对变量 a 执行一次左查询。经过左查询之后,发现变量 a 已经被声明(假如发现变量 a 没有被声明,在非严格模式下,对它赋值的这个操作不会导致引擎报错),所以会继续把 { n: 2 } 赋值给变量 a。之后会把 a = { n: 2 } 语句的返回结果,作为第一个 = 右边的表达式。所以第三行代码变成 ({ n: 1 }).x = { n: 2 } 或者 b.x = { n: 2}。如果没有第二行代码 const b = a;,在执行完第三行代码之后,对象 { n: 1, x: { n: 2} } 所占据的内存会被 GC 回收。补充一句,假设第三行代码就只有 a.x 的话,那么第三行代码的执行过程就结束啦。

至于想搞清楚自己到底有没有理解这个,可以尝试想一下:如果 . 的优先级低于 = 的优先级,上述代码的执行过程是怎样的?

let a = { n: 1 };
const b = a;

// `.` 的优先级低于 `=` 的优先级
a.x = a = { n: 2 };
console.log(a); // 报错

简单的分析一下,a.x = a = { n: 2 }; 这段代码,最后会演变成 (a = { n: 2 }, x = (a = { n: 2 }), a.(x = (a = { n: 2 })))。简化一下会变成这样:(a = { n: 2 }, x = a, a.{ n: 2 })

@marsprince
Copy link

引用类型的原因,我之前写了一下https://github.com/marsprince/lelouch/blob/master/docs/learn/about-point.md

@yeyi361936738
Copy link

连续赋值

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)
  1. a 赋值,a 指向堆内存 {n:1}
a = { n : 1 }
  1. b 赋值,b 也指向对内存 {n:1}
b = a
  1. .的优先级大于=,所以优先赋值。ps:此时a.x已经绑定到了{n: 1 , x: undefined}被等待赋值
a.x = undefined

a // {n: 1 , x: undefined}
b // 也指向上行的堆内存
  1. 同等级赋值运算从右到左,a改变堆内存指向地址,所以a = {n: 2},
a.x = a = {n: 2};
  1. 因为a.x已经绑定到了{n: 1 , x: undefined}这个内存地址,所以相当于
{n: 1 , x: undefined}.x = {n: 2}
  1. 结果
a = {n: 2}
b = {
  n: 1,
  x: {
    n: 2
  }
}

@ruyanzhang
Copy link

之前我也一直想为什么不会重新解析a,然后看了上面很多分析,但还是有些看不明白,然后看到点的优先级大于等号的优先级,那我可以这么理解吗?
var a = {n: 1};
var b = a;
a.x={n:2};
a = {n:2};

@zhuzhh
Copy link

zhuzhh commented Apr 16, 2019

来凑个热闹 ...

把 a.x = a = {n: 2} 拆分来分析
1. [0x001].x = ...
2. a = {n: 2}
3. [0x001] = a 

解析js连续赋值的坑

@ghost
Copy link

ghost commented Apr 16, 2019

@yeyi361936738 我不认同你的这个观点

. 的优先级大于 =,所以优先赋值。ps:此时 a.x 已经绑定到了 {n: 1 , x: undefined} 被等待赋值

你这句话恰好说明你不懂 . 的优先级大于 = 是什么意思

@onloner2012 @ruyanzhang 至于为什么不会重新解析 a,这恰恰是说明 . 操作符的优先级高于 =。因为 . 操作符(a.x)在一开始就已经被执行过,所以这时候你可以把 a.x 理解成 ({ n: 1 }).x。如果引擎在赋值操作(= 操作符属于二元操作符)的过程中,又去访问 a(重新解析 a),势必又会去执行 . 操作符,不就说明 . 操作符的优先级低于 =,这与事实(. 操作符的优先级高于 =)矛盾,所以此时 = 操作符是把 a.x 看作一个整体,不会重新解析 a,不过存在重新解析 a 的情况:

let a = {};
let b;

[b = a] = [, a = {n: 2}];

现在继续来解释下面这一段代码

const a = {};
a.x = void 1024;

由于 . 的优先级大于 =,所以首先执行的是对象属性的 get 操作(又称之为左查询),通过执行该查询操作之后,发现 a 对象本身以及原型链上都不存在该属性(x);此时也意味着 . 操作符的执行先告一段落。假设第二行代码里面没有 = 操作符(a.x;),到这里也就意味着,第三行代码的执行过程全部结束。然后引擎继续从 x 的位置开始向右执行(可能你会问我为什么是向右执行,这是因为不同操作符的执行顺序由该操作符本身的优先级决定的,自然执行完 . 操作符,然后引擎去执行 =,恰好 = 操作符出现在 . 操作符的右边),然后在执行的过程中遇到 = 操作符,由于 = 操作符具有右结合性,也就意味着这时候会首先执行 = 右边的表达式,所以在上述表达式的计算过程结束后,得到计算值 undefined ,并且将该计算值(undefined)赋给 a.x。可能你会问我为什么是赋值?这是因为 = 操作的作用是赋值以及该操作符是二元操作符。正如之前所提到的,{ n: 1 } 或者 b 对象上没有 x 属性,这也就意味着此时的赋值操作是 set 操作,所以最后的结果({ n: 1 } 或者 b 对象上新增一个属性值为 undefined 的属性 x)也正如你所看到的那样。

顺便说一句,往对象上新增一个属性或者修改已存在属性的属性值,不一定能成功。以下是一些🌰:

  1. 对象的原型链上存在同名的 non-writable 属性
Object.defineProperties(
  Object.prototype,
  {
    a: {
      value: 1,
      writable: false,
    },
  },
);

const obj = {};
obj.a = 2;
  1. 对象的原型链上存在同名的属性,该属性不存在 setter
Object.defineProperties(
  Object.prototype,
  {
    a: {
      get() {
        return 1;
      },
    },
  },
);

const obj = {};
obj.a = 2;
  1. 对象处于 non-extensible,表明该对象不能新增属性
const empty = {};
Object.isExtensible(empty); // === true

Object.preventExtensions(empty);
Object.isExtensible(empty); // === false

// Sealed objects are by definition non-extensible.
const sealed = Object.seal({});
Object.isExtensible(sealed); // === false

// Frozen objects are also by definition non-extensible.
const frozen = Object.freeze({});
Object.isExtensible(frozen); // === false

至于为什么对下面这句表述所加粗的地方存在质疑,是因为有🌰的加持

1、优先级 . 的优先级高于 =,所以先执行 a.x堆内存中的 { n: 1 } 就会变成 { n: 1, x: undefined },改变之后相应的 b.x 也变化了,因为指向的是同一个对象。

let a = { n: 1 };
const b = a;
const proxy = new Proxy(a, {
  set(target, key, value, receiver){
    console.info([
      `需要把对象 ${JSON.stringify(target)}${key} 属性的值改为 ${JSON.stringify(value)}`,
      `此时的 a 对象以及 b 对象分别为 ${JSON.stringify(a)}${JSON.stringify(b)}`,
      `target 对象是否等于 b 对象:${target === b}`
    ].join('\n'));
    return Reflect.set(target, key, value, receiver);
  },
});

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

console.log(a.x);	
console.log(b.x);

@fisher-zh
Copy link

a.x = a = { n:2 }
  1. a 添加属性 x,a 原来指向的对象为 { x: undefine, n: 2 }
  2. a 指向新的内存地址 { n:2 },此时 b 仍指向之前旧对象的内存地址
  3. a.x 此时即 b.x,原有旧对象被修改为 { x: { n:2 }, n: 2}

@kexiaofu
Copy link

我在想,问题的表达式能不能改写成这样

var a = { n : 1};
var b = a;

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

console.log(a.x);
console.log(b.x);

因为 . 优先级比 = 的高,那么是否能先提出来,这样情况就比较明显了。

@gadzan
Copy link

gadzan commented Jul 24, 2019

之前我也一直想为什么不会重新解析a,然后看了上面很多分析,但还是有些看不明白,然后看到点的优先级大于等号的优先级,那我可以这么理解吗?
var a = {n: 1};
var b = a;
a.x={n:2};
a = {n:2};

可以看下你楼上的答案:#93 (comment)
主要是在理解a.x = a = {n: 2};的优先级上,

  1. 运行这一行时,a.x已经定义为a={n:2}这个表达式的结果,也就是说,
  2. a.x的实际上是{n:1}这个内存地址的x属性,
  3. 然后,才运行a={n:2}这一步,
  4. 这时候a的代表的内存地址改变了,a的内存地址变成{n:2}的内存地址,
  5. a跟之前一步的a.x代表的内存地址({n:1}这个的内存地址)已经没有关系了,所以a.x的内存实际上跟b一样,相当于1中的b.x被定义为a={n:2}这个表达式的结果,最后结果就是:
a = {n:2}
b = {n:1, x: {n:2}}

@mongonice
Copy link

一开始有点疑惑,
总觉得a.x = a 执行之后是
a.x = {n: 1} 而非 a.x = {n: 2},
不过看了 @yygmind 的解释,顿悟了,原来
a.x = a = {n: 2} 的步骤是:
先执行a.x : a.x = {n: 1, x: undefined}
变执行方向从右到左a = {n: 2}: a = {n: 2};
再继续像左执行 a.x = a: a.x = {n: 1, x: {n: 2}}
此时 a = {n: 2};
b = {n: 1, x: {n: 2}};

@tkctly
Copy link

tkctly commented Aug 23, 2019

console.log(a.x); // undefined
console.log(b.x); //{n:2}

一些相关分析

@shineSnow
Copy link

. 操作符的优先级比赋值高。
本题最难理解的一行代码是
a.x = a = { n:2 }

解构一下发生了什么:

  1. a.x = undefined
  2. a.x = { n: 2 } // 等同于 b.x = { n:2 }
  3. a = { n:2 } // a 的引用指向发生了变化

关键在于,. 操作符的计算会先执行,也就是先把 b.x 引用到了新对象 { n:2 },
再把之前定义的 a 变量重新赋值。

作者:叔叔张
知乎上的这个回答很不错

@callmezhenzhen
Copy link

这一篇讲的很清除,如果再加上a = {n: 2}的返回值为{n: 2}就更好了
https://segmentfault.com/a/1190000008475665

@bin-dogami
Copy link

总感觉大家在强行解释 ,为啥没人觉得这种设计有问题?同样是赋值,a.x 比 a 为啥优先级就一定要高?
理解起来费劲,我懒得记了,面试的时候问到了我就说不知道,我从来不会写这么奇怪的赋值

@Panamer
Copy link

Panamer commented Nov 27, 2019

var a = {n: 1};
var b = a;
a.x = {n: 2}; // 此时b.x = {n: 2};
a = {n: 2};

console.log(a.x)
console.log(b.x)

@yygmind
Copy link
Contributor

yygmind commented Dec 16, 2019

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)

@lovelmh13
Copy link

考察对象的指针和符号的优先级

  • var b = a; b 和 a 都指向同一个地址。
  • .的优先级高于=。所以先执行a.x,于是现在的ab都是{n: 1, x: undefined}
  • =是从右向左执行。所以是执行 a = {n: 2},于是a指向了{n: 2}
  • 再执行 a.x = a。 这里注意,a.x 是最开始执行的,已经是{n: 1, x: undefined}这个地址了,而不是一开的的那个a,所以也就不是{n: 2}了。而且b和旧的a是指向一个地址的,所以b也改变了。
  • 但是,=右面的a,是已经指向了新地址的新a
  • 所以,a.x = a 可以看成是{n: 1, x: undefined}.x = {n: 2}

得出

a = { n: 2 }
b = {
         n: 1,
         x: { n: 2 }
}

最终结果打印出来是: undefined { n: 2 }

@random-yang
Copy link

备注 2020年3月18日

@lianglixiong
Copy link

image

@soraly
Copy link

soraly commented Jun 23, 2020

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) 	
console.log(b.x)
  • 考察的第一个知识点,.运算符优先级比=高,所以a.x执行后,此时a和b对象就指向{n:1, x: undefined}的地址
  • 然后=运算符是从右往左执行,所以a被赋值成了 {n: 2}
  • 再执行 a.x = {n: 2} 时,因为b和a指向同一个地址, 相当于
    {n:1, x: undefined}.x = {n:2},也就是 b.x = {n: 2}
  • 所以最终b.x = {n: 2}

@Yayure
Copy link

Yayure commented Mar 10, 2021

第一步a、b均指向{n: 1}
第二步进行连续赋值操作:
js连续赋值操作规则为先从左往右计算被赋值的值的位置,然后再从右往左进行赋值
此处优先计算出x的赋值位置即{n: 1}和a的被赋值位置,然后再进行赋值操作,a被赋值为{n: 2},x也被赋值为{n: 2}且赋值位置为{n: 1}
所以a.x为undefined
由于b指向{n: 1},故访问b时x值为{n: 2}

js连续赋值规则论证如下:

上述例子可转换为:

(function() {
  var obj = { a: { c: { n: 1 } } };
  var b = obj.a.c;
  obj.a.c.x = obj.a.c = { n: 2 };
  console.log(obj.a.c.x);
  console.log(b.x);
})();

添加监听后为:

(function() {
  var obj = {};
  var model = { 'a': { c: { n: 1 } } };
  model['a.c'] = model['a'].c;
  Object.defineProperty(obj, 'a', {
    set: function(val) {
      console.log('set a');
      model['a'] = val;
    },
    get: function() {
      console.log('get a');
      return model['a'];
    }
  });
  Object.defineProperty(obj.a, 'c', {
    set: function(val) {
      console.log('set a.c');
      model['a.c'] = val;
    },
    get: function() {
      console.log('get a.c');
      return model['a.c'];
    }
  });
  var b = obj.a.c;
  Object.defineProperty(obj.a.c, 'x', {
    set: function(val) {
      console.log('set a.c.x');
      model['a.c.x'] = val;
    },
    get: function() {
      console.log('get a.c.x');
      return model['a.c.x'];
    }
  });
  console.log('***start***');
  obj.a.c.x = obj.a.c = { n: 2 };
  // obj.a.c = obj.a.c.x = {n: 2};
  console.log('***end***');
  console.log(obj.a.c.x);
  console.log(b.x);
})();

obj.a.c.x = obj.a.c = {n: 2}; 输出:

...
***start***
get a
get a.c
get a
set a.c
set a.c.x
***end***
...
undefined
...
{n: 2}

obj.a.c = obj.a.c.x = {n: 2};输出:

...
***start***
get a
get a
get a.c
set a.c.x
set a.c
***end***
...
undefined
...
{n: 2}

根据观察结果先从左往右查找赋值位置,位置查找完后再从右往左对查找到的位置进行赋值

注:此处的从右往左进行赋值并不是简单的每项逐一赋值。而是以拆开的形式统一赋值,如x = y = z实际为y = z, x = z。此处在MDN上有相关说明

如需验证可执行:

(function() {
  var a = {};
  var b = {};
  Object.defineProperty(a, 'x', {
    get: function() {
      return 1
    }
  });
  b = a.x = { n: 2 };
  console.log(b);
})();
{n: 2}

@DemonKHH
Copy link

DemonKHH commented Apr 26, 2021

简单理解:
a.x = a = {n: 2} 拆分理解
a = {n: 2} 这里是重新定义了一个变量a 地址与第一次定义的变量a不一致( 可以尝试使用let 定义a,会出现报错表示a已经定义了
a.x 这里相当于b.x 是最开始定义的a,可以理解为
a = {n: 2} ;
b.x = a;

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

console.log(a.x) // {n: 2}	
console.log(b.x) // {n: 1, x: {n: 2}}
console.log(b.x === a)  // true 

@Yangfan2016
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests