Daily-Interview-Question icon indicating copy to clipboard operation
Daily-Interview-Question copied to clipboard

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

Open zeroone001 opened this issue 5 years ago • 33 comments

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

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

这是连续赋值的坑

zeroone001 avatar Apr 11 '19 23:04 zeroone001

结果: 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 才会得到创建。

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

Chorer avatar Apr 12 '19 00:04 Chorer

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

jsonz1993 avatar Apr 12 '19 00:04 jsonz1993

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

bran-nie avatar Apr 12 '19 01:04 bran-nie

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

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

YouziXR avatar Apr 12 '19 01:04 YouziXR

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

kailiu999 avatar Apr 12 '19 01:04 kailiu999

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

WQHASH avatar Apr 12 '19 01:04 WQHASH

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 }

JonathonChen avatar Apr 12 '19 02:04 JonathonChen

为啥不会重新解析a啊

onloner2012 avatar Apr 12 '19 03:04 onloner2012

image

image

Zyingying avatar Apr 12 '19 07:04 Zyingying

有一个网站可以将 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 })

ghost avatar Apr 12 '19 13:04 ghost

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

marsprince avatar Apr 15 '19 11:04 marsprince

连续赋值

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
  }
}

yeyi361936738 avatar Apr 16 '19 02:04 yeyi361936738

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

ruyanzhang avatar Apr 16 '19 03:04 ruyanzhang

来凑个热闹 ...

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

解析js连续赋值的坑

zhuzhh avatar Apr 16 '19 05:04 zhuzhh

@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);

ghost avatar Apr 16 '19 08:04 ghost

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}

fisher-zh avatar May 30 '19 08:05 fisher-zh

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

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

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

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

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

kexiaofu avatar Jul 10 '19 01:07 kexiaofu

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

可以看下你楼上的答案:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/93#issuecomment-483490014 主要是在理解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}}

gadzan avatar Jul 24 '19 03:07 gadzan

一开始有点疑惑,
总觉得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}};

mongonice avatar Jul 24 '19 09:07 mongonice

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

一些相关分析

tkctly avatar Aug 23 '19 02:08 tkctly

. 操作符的优先级比赋值高。 本题最难理解的一行代码是 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 变量重新赋值。

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

shineSnow avatar Aug 25 '19 01:08 shineSnow

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

callmezhenzhen avatar Sep 09 '19 01:09 callmezhenzhen

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

bin-dogami avatar Nov 08 '19 09:11 bin-dogami

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)

Panamer avatar Nov 27 '19 08:11 Panamer

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

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

yygmind avatar Dec 16 '19 02:12 yygmind

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

  • 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 }

lovelmh13 avatar Feb 03 '20 09:02 lovelmh13

备注 2020年3月18日

random-yang avatar Mar 18 '20 02:03 random-yang

image

lianglixiong avatar Apr 20 '20 04:04 lianglixiong

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}

soraly avatar Jun 23 '20 07:06 soraly

第一步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}

Yayure avatar Mar 10 '21 07:03 Yayure

简单理解: 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 

DemonKHH avatar Apr 26 '21 07:04 DemonKHH

image image

运算符 ’.‘ 优先级 大于 '='

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#%E6%B1%87%E6%80%BB%E8%A1%A8

Yangfan2016 avatar Aug 08 '22 03:08 Yangfan2016