有這樣一個熱門問題:javascript
var a = {n: 1}; var b = a; a.x = a = {n: 2}; alert(a.x); // --> undefined alert(b.x); // --> {n: 2}
其實這個問題很好理解,關鍵要弄清下面兩個知識點:java
JS引擎對賦值表達式的處理過程git
賦值運算的右結合性github
形如segmentfault
A = B
的表達式稱爲賦值表達式。其中A和B又分別能夠是表達式。B能夠是任意表達式,可是A必須是一個左值。函數
所謂左值,就是能夠被賦值的表達式,在ES規範中是用內部類型引用(Reference)描述的。例如:es5
表達式foo.bar
能夠做爲一個左值,表示對foo這個對象中bar這個名稱的引用;spa
變量email
能夠做爲一個左值,表示對當前執行環境中的環境記錄項envRec中email這個名稱的引用;code
一樣地,函數名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
了。
最後用一張圖表示整個表達式的運算過程: