更新(1/23/2018)
寫在前面,本文讓讀者產生了誤會。有這樣一些原因:
- JS為何能取到地址值。
a.x = a
未解釋清楚。- .運算子是否會對賦值運算有所干擾。
首先:本文用addr只是一個代稱,表達的是該地址對應的那塊記憶體。 關於2.3點就是本次更新的原因
a.x = a
賦值表示式先確定左值(可以這樣理解,如果不確定我要去的地方,取到值又有什麼用呢?左邊的值在執行賦值之前就已經確定了),然後再將右邊表示式的返回值給到左值。那麼a.x = a = {n : 2}
就是從左往右先確定a.x
再確定a
,然後將返回值從右往左依次賦值給左邊。- .運算子會對賦值運算子有優先順序干擾嗎?(會因為a.x優先順序高於a = {n : 2}而先執行a.x = a嗎?)下面我來嘗試一下:
將題目改寫成:
var a = {n: 1};
var b = a;
a.x = a = a.y = {n: 2};
//改寫成這樣,那麼怎麼確定優先順序呢?
複製程式碼
現在按照我文章的思路來:
- 先確定所有左邊的地址值(再次強調指的是對應的記憶體!):
addr(a.x) = addr(a) = addr(a.y) = addr({n : 2})
- 我們假設:
addr(a) = 0x100,
addr(a.x) = 0x101,
addr(a.y) = 0x102,
addr({n : 2}) = 0x888,
addr({n : 1}) = 0x999
複製程式碼
2.(從右往左)將右邊的值賦值給左值,然後將其作為該賦值表示式的值返回:
1. 先執行:addr(a.y) = addr({n : 2}),將{n : 2}的地址值存放在addr(a.y)這個地址值對應的記憶體!中。
(別用箭頭指,容易混淆,簡單的當做賦值就好了。本文就是犯得這個錯,導致沒有說清楚。)
2. 然後將addr(a.y) = addr({n : 2})的右值作為該表示式的返回值N返回,在這裡會作為下一個賦值表示式的右值。
3. 接著執行addr(a) = N,同上返回N。
4. 接著執行addr(a.x) = N,返回N。
然後我們執行
console.log(a); // {n : 2}
console.log(a.x); // undefined
console.log(a.y); // undefined
console.log(b.x); // {n : 2}
console.log(b.y); // {n : 2}
複製程式碼
不對呀,為什麼給a.x和a.y的地址值對應的記憶體賦值了,但卻沒有東西,你是不是講錯了?
因為此時a(0x100)這塊記憶體存的是對{n : 2}的引用,它會去0x888這塊記憶體中找,這樣肯定找不著x和y,因為他們在0x999記憶體中。
而b在一開始var b = a的時候,就將a(0x100)這塊記憶體中存的(0x999)拷貝過來。而y和x就在0x999對應的記憶體下。所以b找的到。
希望本次更新對有困惑朋友有所幫助!(也許是我講複雜了,希望多多提問。)
有這樣一道面試題
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // undefined
alert(b.x); // [object, Object]
複製程式碼
一開始沒有太好思路,或者說是沒有想明白,經過一番折騰,算是整理清楚了思路,接下來會一一講明白,希望能對其他人有所幫助。
開始之前,需要清楚賦值表示式是怎麼執行的。首先先明白什麼是什麼是右結合性和什麼是賦值表示式:
右結合性
賦值運算子是右結合性的,如果不知道,就請記住啦! 形如:
A = B = C = D
等價於
A = (B = (C = D))
賦值表示式
A = B
這就是一個賦值表示式,並且一個賦值表示式存在一個左值和一個右值,這可不是胡編亂造的,我們們說話有理有據:
引用連結(11.13.1 Simple Assignment ( = ) )
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
- Let lref be the result of evaluating LeftHandSideExpression.
- Let rref be the result of evaluating AssignmentExpression.
- Let rval be GetValue(rref).
- Throw a SyntaxError exception if the following conditions are all true:
- Type(lref) is Reference is true
- IsStrictReference(lref) is true
- Type(GetBase(lref)) is Environment Record
- GetReferencedName(lref) is either "eval" or "arguments"
- Call PutValue(lref, rval).
- Return rval.
翻譯過來:
大體來說 賦值表示式:左邊的表示式 = 賦值表示式 具體評判的步驟如下:
- 將比左邊的表示式的值稱為'lref'
- 將右邊賦值表示式的值稱為'rref'
- 將'rval' 作為GetValue(rref)'的返回值
- 如果下列情況為true就會報錯
- balala~~~~
- 呼叫 PutValue(lref, rval)
- 返回'rval'
由上面可以清楚的知道表示式運算的流程:
- 先計算左邊的表示式(很重要,先計算左邊的!這裡計算的是左邊表示式在記憶體中的地址值)
- 在計算右邊的表示式(先左後右,這裡計算的是右邊的值,這裡的值就是值,並不特指地址!若是物件則為地址值)
- 計算右邊的值
(等等,這裡有疑問,為什麼運算兩遍右邊?第一遍是計算右邊表示式的(+ — * /)所得到的值,第二遍是返回這個結果值,也就是每個表示式都有返回值!)
- 這裡balala~~~
- 呼叫函式PutValue,這個做的才是給值,就是將右邊的返回值rval放進左邊的lref對應的地址值(也就是對應的記憶體)
- 然後返回這個表示式的值,也就是rval(這裡再次說明表示式有返回值,而且還可以看出這個值是GetValue計算出的右邊的值,說白了就是等號右邊的值)
如果還沒理解的話,沒關係,往下看,我會畫圖幫助理解。
有了以上兩個知識點,下面來分析一下題目。
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // undefined
alert(b.x); // [object, Object]
複製程式碼
真正難以理解的是在第三句程式碼a.x = a = {n: 2}
下面我們們開工吧!
-
第一、二句程式碼執行之後,記憶體圖如下:
-
第三句程式碼先進行改寫
a.x = a = {n: 2};
//先右結合性
a.x = (a = {n : 2})
//在計算等號左邊的值
addr(a.x) = (addr(a) = {n : 2})
//在計算等號右邊的值
addr(a.x) = ( addr(a) = value( {n : 2} ) )
複製程式碼
如圖:
如上圖來進行運算
addr(a.x) = ( addr(a) = value( {n : 2} ) )
- 先計算所有等號左邊的值:addr(a.x)的值為:0x8889、addr(a)的值為:0x0001
(0x8889 <-- (0x0001 <-- (0x9999)))
- 在計算{n : 2}的值為:0x9999(因為{n : 2}是一個物件,所以在這裡計算得到的是地址值)
- 將0x9999作為當前{n : 2}的返回值(也就是當前表示式的右邊的值)
- 將返回值賦值給0x0001(對應的記憶體)
- 返回( addr(a) = value( {n : 2} ) )的值:0x9999
- 將返回值賦值給0x8889(對應記憶體)
- 返回addr(a.x) = ( addr(a) = value( {n : 2} ) )的值: 0x9999
一開始找到當前表示式左右左邊的值,也就是他們地址值。然後依次將右值逐個放到對應的記憶體!
最後因為a的地址是指向0x9999的,且其不存x這個屬性,故返回undefined。而b的地址指向裡面存在一個儲存{n:2}的地址的x屬性,故返回的是物件。