一文詳解令人抓狂的JS賦值順序
前言
你可能以為自己已經(jīng)非常熟悉 JavaScript 的賦值操作了,但這一行代碼:
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
卻讓無數(shù)開發(fā)者陷入沉思:到底是先給 a.x 賦值,還是先給 a 重新賦值?為什么 console.log(b.x) 是一個對象,而 a.x 卻是 undefined?這不是語法問題,而是 JavaScript 底層執(zhí)行機制的奇妙。
下面就簡單講述一下。
JavaScript 賦值操作基礎(chǔ)
運算符優(yōu)先級與結(jié)合性
賦值運算符(=、+=、-=) 在 JavaScript 中是 右結(jié)合(right-to-left)的。這意味著表達式 a = b = c 會先計算右側(cè)的 b = c,再把結(jié)果賦給 a。
| 運算符 | 優(yōu)先級 | 結(jié)合性 |
|---|---|---|
= | 3 | 右到左 |
| +, - | 13 | 左到右 |
. | 20 | 左到右 |
左值(L-value)與右值(R-value)
- 左值:表達式的“賦值目標(biāo)”,例如變量、對象屬性、數(shù)組元素。
- 右值:表達式的“計算結(jié)果”,例如字面量、函數(shù)返回值、其他表達式。
賦值時,JS 引擎要分別評估左值(確定要寫到哪里)和評估右值(計算要寫什么),然后才執(zhí)行寫入。
連續(xù)賦值語義
為什么 a = b = c 能連續(xù)寫?
因為在 JS 中,賦值表達式本身會返回被賦的值,所以可以寫成鏈式:
// 步驟拆解: b = c // 返回 c a = (b = c) // 再把 c 賦給 a
賦值表達式的返回值
- 表達式 x = y 的求值結(jié)果就是“被賦的值 y”。
- 因此 a = b = 5 相當(dāng)于 a = 5; b = 5;,且整個表達式的值為 5。
a = b = 5; console.log(a, b, (a = b = 5)); // 5 5 5
ECMAScript 規(guī)范視角
如何評估賦值表達式
根據(jù) ECMAScript® Language Specification(ECMA-262):
- 評估 AssignmentExpression:先識別運算符右側(cè)的“AssignmentExpression”
- 評估左值:調(diào)用 GetValue 獲取右側(cè)的值
- 分辨目標(biāo):調(diào)用 PutValue 將值寫入左值目標(biāo)
- 返回值:整個表達式的值為所寫入的值
步驟拆解
Function EvaluateAssignmentExpression(expr):
If expr is “LeftHandSide = RightHandSide” then:
// 1. 先評估右側(cè),得到一個值 R
R = Evaluate(RightHandSide)
// 2. 再評估左側(cè),得到一個引用 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
執(zhí)行順序:
- z = 100 → 返回 100
- y = (z = 100) → 返回 100
- x = (y = (z = 100)) → 返回 100
2、對象屬性與賦值順序
let obj = { a: 1 };
obj.b = obj.a = 2;
console.log(obj); // { a: 2, b: 2 }執(zhí)行順序:
- 先計算 obj.a = 2,寫入 a,返回 2。
- 再執(zhí)行 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 }; - 解析左側(cè)目標(biāo)(Capture LHS)
- 引擎先看到整個賦值表達式 a.x = …,此時 a 仍指向最初的對象 [Obj1]。
- “目標(biāo)位置”鎖定為:對象 [Obj1] 的屬性 x。
- 執(zhí)行右側(cè)賦值(Evaluate RHS)
- 新建一個對象字面量 { n:2 },記為 [Obj2]。
- 把 [Obj2] 賦給變量 a,此刻 a 從指向 [Obj1] 變?yōu)橹赶?[Obj2]。
- 賦值表達式 a = [Obj2] 的返回值是這個新對象 [Obj2]。
- 寫入屬性(Perform original “.x = …”)
- 雖然此時 a 已經(jīng)指向 [Obj2],但左側(cè)“目標(biāo)”早已捕獲,仍是 [Obj1].x 這個位置。
- 因此,將右側(cè)返回的 [Obj2] 寫入到原始對象 [Obj1] 的 x 屬性上:Obj1.x = Obj2。
- 查看最終結(jié)果
- a 指向 [Obj2],所以 a.x 查的是 [Obj2].x ——該對象上沒有 x 屬性,結(jié)果 undefined。
- b 依舊指向最初的 [Obj1],它的 x 屬性被寫入了 [Obj2],所以 console.log(b.x) 輸出 { n: 2 }。
關(guān)鍵點:
- 左側(cè)目標(biāo)先捕獲:賦值表達式開始時就確定屬性寫入位置,不受后續(xù)變量指向變化影響。
- 左側(cè) a.x 在賦值之前就要解析“引用”,此時 a 仍指向舊對象;
- 右側(cè) a = tmp 改變了 a 引用,但不會影響已經(jīng)捕獲的“屬性寫入位置”。
- 賦值表達式返回值:a = {n:2} 返回新的對象引用,用于后續(xù)“屬性賦值”。
- 順序原則:先捕獲 LHS,再執(zhí)行 RHS 并返回值,最后將返回值寫入先前捕獲的 LHS。
這樣,一眼就能看清變量指向與屬性寫入的“先后關(guān)系”,避免踩坑。
總結(jié)
到此這篇關(guān)于JS賦值順序的文章就介紹到這了,更多相關(guān)JS賦值順序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Add a Table to a Word Document
Add a Table to a Word Document...2007-06-06
微信小程序web-view環(huán)境下H5跳轉(zhuǎn)小程序頁面方法實例代碼
微信小程序是一種全新的連接用戶與服務(wù)的方式,它可以在微信內(nèi)被便捷地獲取和傳播,同時具有出色的使用體驗,下面這篇文章主要給大家介紹了關(guān)于微信小程序web-view環(huán)境下H5跳轉(zhuǎn)小程序頁面方法的相關(guān)資料,需要的朋友可以參考下2022-08-08

