令人抓狂的 JS 赋值顺序!

你可能以为自己已经非常熟悉 JavaScript 的赋值操作了,但这一行代码:

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

却让无数开发者陷入沉思:到底是先给 a.x 赋值,还是先给 a 重新赋值?为什么 console.log(b.x) 是一个对象,而 a.x 却是 undefined?这不是语法问题,而是 JavaScript 底层执行机制的奇妙。

下面就简单讲述一下。

JavaScript 赋值操作基础

运算符优先级与结合性

赋值运算符(=、+=、-=) 在 JavaScript 中是 右结合(right-to-left)的。这意味着表达式 a = b = c 会先计算右侧的 b = c,再把结果赋给 a。

运算符优先级结合性
=3右到左
+, -13左到右

.

20左到右

左值(L-value)与右值(R-value)

  •  左值:表达式的“赋值目标”,例如变量、对象属性、数组元素。
  • 右值:表达式的“计算结果”,例如字面量、函数返回值、其他表达式。

赋值时,JS 引擎要分别评估左值(确定要写到哪里)和评估右值(计算要写什么),然后才执行写入。

连续赋值语义

为什么 a = b = c 能连续写?

因为在 JS 中,赋值表达式本身会返回被赋的值,所以可以写成链式:

// 步骤拆解:
b = c        // 返回 c
a = (b = c)  // 再把 c 赋给 a

 赋值表达式的返回值

  • 表达式 x = y 的求值结果就是“被赋的值 y”。
  • 因此 a = b = 5 相当于 a = 5; b = 5;,且整个表达式的值为 5。
a = b = 5;
console.log(a, b, (a = b = 5)); // 5 5 5

ECMAScript 规范视角

如何评估赋值表达式

根据 ECMAScript® Language Specification(ECMA-262):

  1. 评估 AssignmentExpression:先识别运算符右侧的“AssignmentExpression”
  2. 评估左值:调用 GetValue 获取右侧的值
  3. 分辨目标:调用 PutValue 将值写入左值目标
  4. 返回值:整个表达式的值为所写入的值
步骤拆解
Function EvaluateAssignmentExpression(expr):
  If expr is “LeftHandSide = RightHandSide” then:
    // 1. 先评估右侧,得到一个值 R
    R = Evaluate(RightHandSide)
    // 2. 再评估左侧,得到一个引用 L
    L = ResolveReference(LeftHandSide)
    // 3. 将 R 写入 L
    PutValue(L, R)
    // 4. 返回 R 作为整个表达式的值
    return R
常见示例

1、简单链式赋值

let x, y, z;
x = y = z = 100;
console.log(x, y, z); // 100 100 100

执行顺序:

  1. z = 100  → 返回 100
  2. y = (z = 100) → 返回 100
  3. x = (y = (z = 100)) → 返回 100

2、对象属性与赋值顺序

let obj = { a: 1 };
obj.b = obj.a = 2;
console.log(obj); // { a: 2, b: 2 }

执行顺序:

  1.  先计算 obj.a = 2,写入 a,返回 2。
  2. 再执行 obj.b = <返回值 2>。
重点示例分析
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };

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

详解解析:

var a = { n: 1 };  // [Obj1]
var b = a;         // b ➔ [Obj1]
a.x = a = { n: 2 };  
  1. 解析左侧目标(Capture LHS)
    1. 引擎先看到整个赋值表达式 a.x = …,此时 a 仍指向最初的对象 [Obj1]。
    2. “目标位置”锁定为:对象 [Obj1] 的属性 x。
  2. 执行右侧赋值(Evaluate RHS)
    1. 新建一个对象字面量 { n:2 },记为 [Obj2]。
    2. 把 [Obj2] 赋给变量 a,此刻 a 从指向 [Obj1] 变为指向 [Obj2]。
    3. 赋值表达式 a = [Obj2] 的返回值是这个新对象 [Obj2]。
  3. 写入属性(Perform original “.x = …”)
    1. 虽然此时 a 已经指向 [Obj2],但左侧“目标”早已捕获,仍是 [Obj1].x 这个位置。
    2. 因此,将右侧返回的 [Obj2] 写入到原始对象 [Obj1] 的 x 属性上:Obj1.x = Obj2。
  4. 查看最终结果
    1. a 指向 [Obj2],所以 a.x 查的是 [Obj2].x ——该对象上没有 x 属性,结果 undefined。
    2. b 依旧指向最初的 [Obj1],它的 x 属性被写入了 [Obj2],所以 console.log(b.x) 输出 { n: 2 }。

关键点:

  • 左侧目标先捕获:赋值表达式开始时就确定属性写入位置,不受后续变量指向变化影响。
    • 左侧 a.x 在赋值之前就要解析“引用”,此时 a 仍指向旧对象;
    • 右侧 a = tmp 改变了 a 引用,但不会影响已经捕获的“属性写入位置”。
  • 赋值表达式返回值:a = {n:2} 返回新的对象引用,用于后续“属性赋值”。
  • 顺序原则:先捕获 LHS,再执行 RHS 并返回值,最后将返回值写入先前捕获的 LHS。

这样,一眼就能看清变量指向与属性写入的“先后关系”,避免踩坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值