JS連等賦值
轉載出處:https://segmentfault.com/a/1190000004224719
有這樣一個熱門問題:
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // --> undefined
alert(b.x); // --> {n: 2}
其實這個問題很好理解,關鍵要弄清下面兩個知識點:
-
JS引擎對賦值表示式的處理過程
-
賦值運算的右結合性
一. 賦值表示式
形如
A = B
的表示式稱為賦值表示式。其中A和B又分別可以是表示式。B可以是任意表達式,但是A必須是一個左值
所謂左值,就是可以被賦值的表示式,在ES規範中是用內部型別引用(Reference)描述的。例如:
-
表示式
foo.bar
可以作為一個左值,表示對foo這個物件中bar這個名稱的引用; -
變數
email
可以作為一個左值,表示對當前執行環境中的環境記錄項envRec中email這個名稱的引用; -
同樣地,函式名
func
可以做左值,然而函式呼叫表示式func(a, b)
不可以。
那麼JS引擎是怎樣計算一般的賦值表示式 A = B
的呢?簡單地說,按如下步驟:
-
計算表示式A,得到一個引用
refA
; -
計算表示式B,得到一個值
valueB
-
將
valueB
賦給refA
指向的名稱繫結; -
返回
valueB
。
二. 結合性
所謂結合性,是指表示式中同一個運算子出現多次時,是左邊的優先計算還是右邊的優先計算。
賦值表示式是右結合的。這意味著:
A1 = A2 = A3 = A4
等價於
A1 = (A2 = (A3 = A4))
三. 連等的解析
好了,有了上面兩部分的知識。下面來看一下JS引擎是怎樣運算連等賦值表示式的。
以下面的式子為例:
Exp1 = Exp2 = Exp3 = Exp4
首先根據右結合性,可以轉換成
Exp1 = (Exp2 = (Exp3 = Exp4))
然後,我們已經知道對於單個賦值運算,JS引擎總是先計算左邊的運算元,再計算右邊的運算元。所以接下來的步驟就是:
-
計算Exp1,得到Ref1;
-
計算Exp2,得到Ref2;
-
計算Exp3,得到Ref3;
-
計算Exp4,得到Value4。
現在變成了這樣的:
Ref1 = (Ref2 = (Ref3 = Value4))
接下來的步驟是:
-
將Value4賦給Exp3;
-
將Value4賦給Exp2;
-
將Value4賦給Exp1;
-
返回表示式最終的結果Value4。
注意:這幾個步驟體現了右結合性。
總結一下就是:
先從左到右解析各個引用,然後計算最右側的表示式的值,最後把值從右到左賦給各個引用。
四. 問題的解決
現在回到文章開頭的問題。
首先前兩個var語句執行完後,a
和b
都指向同一個物件{n: 1}
(為方便描述,下面稱為物件N1)。然後來看
a.x = a = {n: 2};
根據前面的知識,首先依次計算表示式a.x
和a
,得到兩個引用。其中a.x
表示物件N1中的x,而a
相當於envRec.a
,即當前環境記錄項中的a。所以此時可以寫出如下的形式:
[[N1]].x = [[encRec]].a = {n: 2};
其中,[[]]
表示引用指向的物件。
接下來,將{n: 2}
賦值給[[encRec]].a
,即將{n: 2}
繫結到當前上下文中的名稱a
。
接下來,將同一個{n: 2}
賦值給[[N1]].x
,即將{n: 2}
繫結到N1中的名稱x
。
由於b
仍然指向N1
,所以此時有
b <=> N1 <=> {n: 1, x: {n: 2}}
而a
被重新賦值了,所以
a <=> {n: 2}
並且
a === b.x
五. 最後的最後
如果你明白了上面所有的內容,應該會明白a.x = a = {n:2};
與b.x = a = {n:2};
是完全等價的。因為在解析a.x
或b.x
的那個時間點
。a
和b
這兩個名稱指向同一個物件,就像C++中同一個物件可以有多個引用一樣。而在這個時間點
之後,不論是a.x
還是b.x
,其實早就不存在了,它已經變成了那個記憶體中的物件.x
了。
最後用一張圖表示整個表示式的運算過程: