關於 JavaScript 常見的 Multiple left-hand assignment 考題

前言

Multiple left-hand assignment(連續左側賦值) 可以說是 JavaScript 常見考題,而這又稱之為連續賦值(Chain Variable Assignments),例如:a = b = c 這種讓人匪夷所思的題目,所以就寫一篇關於這一個到底是怎麼運作的。

連續左側賦值(Multiple left-hand assignment)

「Multiple left-hand assignment」中文是連續左側賦值,相信你應該會看過一些考題是這種

1
2
3
4
5
var a = 1;
var b = 2;
var c = 3;

a = b = c;

基本上這個題目算是很簡單簡單,但是本身涵蓋的觀念就不少。

舉例來講等號運算子的相依性是以右至左,因此 c 會優先被賦予到 b,而這賦予的過程是一個表達式,因此 a 就會吃下 b = c 表達式的結果也就是 3

拆開來看的話,就會變成以下

1
2
3
4
5
6
7
8
var a = 1;
var b = 2;
var c = 3;

a = (b = c);
console.log(a); // 3
console.log(b); // 3
console.log(c); // 3

或者你也可以看成以下

1
2
3
4
5
6
7
8
9
var a = 1;
var b = 2;
var c = 3;

b = c;
a = b;
console.log(a); // 3
console.log(b); // 3
console.log(c); // 3

但是 JavaScript 是一個很美麗(機車)的語言,類似的題目只需要稍微調整一下就可以變成另一個全新考題

1
2
3
4
var a = b = c = 1;
console.log(a); // 1
console.log(b); // 1
console.log(c); // 1

因此一個「Multiple left-hand assignment」概念是可以在 JavaScript 中驗證相當多觀念,拿剛剛的 var a = b = c = 1; 來講,就可以看成是這樣

1
var var1 = (var2 = (var3 = 1));

雖然結果都會是 1,但是如果再稍微調整一下題目你會發現一個奇妙的狀況

1
2
3
4
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
console.log(c); // 因為前面出錯而不會出現
var a = b = c = 1;

那麼這個問題是出在哪裡了?主要是出在 var 語法的提升(Hoisting) 導致,因此你可以使用創造與執行兩階段概念來拆解理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 原始程式碼
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
console.log(c); // 因為前面出錯而不會出現
var a = b = c = 1;

// 創造階段
// var 會有提升問題,因此僅有 a 被提升
var a;

// 執行階段
console.log(a); // 因為 a 已經被提升被賦予預設記憶體,因此出現 「undefined」
console.log(b); // 這個階段 b 並沒有被賦予值與宣告,因此會出現「ReferenceError: b is not defined」
// 因錯誤沒辦法往下執行
console.log(c);
c = 1;
b = c;
a = b;

那麼這時候我們再拿六角學院 JavaScript 核心篇中的考題來看一下

1
2
3
4
5
6
var a = { x: 1 };
var b = a;
a.x = { x: 2};
a.y = a = { y: 1};
console.log(a); // 結果?
console.log(b); // 結果?

基本上我們都知道 JavaScript 採用的是 Call by Sharing,因此物件會是採用傳參考概念來運作,接下來讓我們一步一步拆解上面考題

第一步驟

第二步驟

第三步驟

第四步驟稍微會有一點難度,我們先看 a = { y: 1 } 的部分就好

第四步驟

接下來讓我們先暫停一下,剛才前面有講到「Multiple left-hand assignment」的一些拆解與觀念,因此你在看最後一行 a.y = a = { y: 1}; 時可能無意識會這樣拆解

1
2
a = { y: 1};
a.y = a;

或者是這樣

1
a.y = (a = { y: 1});

所以你可能會很興奮的說 console.log(a); 非常簡單答案就是 { y: 1 }

console.log(a) 結果

但是 console.log(b),就稍微有一點困難了,讓我們看一下剛剛的表

第五步驟

我們可以發現 b 依然是指向 0x01,而 0x01 中的 x 被指向到 0x02,因此其中一個一定是 { x: { x: 2 } }

等等你發現了嗎?我為什麼會「其中一個」呢?因為這邊絕對有蹊蹺。

主要原因在於 a.y = a = { y: 1}; 這一段非常陰險!因為代誌不是憨人想得那麼簡單,你可能會想說 a 因為被賦予到新的記憶體位置 0x03,接下來又替 a 新增一個屬性叫做 y 然後指向到 0x03 的位置,畢竟你可能是這樣去拆解

1
a.y = (a = { y: 1});

或者是這樣

1
2
a = { y: 1 };
a.y = a;

但這邊絕對是錯的,因此先讓我們先看一下 JavaScript 的優先性與相依性的部分,我們可以發現「點(.)運算子」的優先性會非常高,所以 a.y 必定會優先被執行,而這時候的 a 並沒有被覆蓋記憶體,因此取出來的 a 會是保持原本的記憶體位置也就是 0x01,但因為屬性還沒有給予值,所以這時候 JavaScript 會預設給予 undefined,接下來才會「正式進入」賦值階段。

那麼在說明「賦值階段」之前請務必切記 b 目前指向的記憶體是 0x01,因此 a 被重新賦予 { y: 1}(新記憶體空間 0x03),但是 0x01 並沒有被釋放掉,為什麼沒有被釋放掉呢?主要原因是 JavaScript 解析器認為你這個記憶體還有被指向著,因此 0x01 並不會被釋放,那麼以程式碼角度來看就會是這樣

1
2
a = { y: 1} (0x03);
a(0x01).y = 0x03;

所以最終的 console.log(b) 答案就會是 { x: { x:2 } y: { y:1 } }

最終解答

1
2
3
4
5
6
var a = { x: 1 };
var b = a;
a.x = { x: 2};
a.y = a = { y: 1};
console.log(a); // { y: 1 }
console.log(b); // { x: { x: 2 }, { y: { y: 1 } } }

如果用 AST 抽象語法樹來看的話就會是這樣

AST 抽象語法樹

你也可以試著將語法貼到以下網址運作一次就會很清楚了

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你也可以考慮請我喝一杯咖啡

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ