解析一道JS面試題

Pomelo1213發表於2018-01-22

更新(1/23/2018)

寫在前面,本文讓讀者產生了誤會。有這樣一些原因:

  1. JS為何能取到地址值。
  2. a.x = a未解釋清楚。
  3. .運算子是否會對賦值運算有所干擾。

首先:本文用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};
//改寫成這樣,那麼怎麼確定優先順序呢?
複製程式碼

現在按照我文章的思路來:

  1. 先確定所有左邊的地址值(再次強調指的是對應的記憶體!):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:

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. Let rref be the result of evaluating AssignmentExpression.
  3. Let rval be GetValue(rref).
  4. 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"
  1. Call PutValue(lref, rval).
  2. Return rval.

翻譯過來:

大體來說 賦值表示式:左邊的表示式 = 賦值表示式 具體評判的步驟如下:

  1. 將比左邊的表示式的值稱為'lref'
  2. 將右邊賦值表示式的值稱為'rref'
  3. 將'rval' 作為GetValue(rref)'的返回值
  4. 如果下列情況為true就會報錯
  • balala~~~~
  1. 呼叫 PutValue(lref, rval)
  2. 返回'rval'

由上面可以清楚的知道表示式運算的流程:

  1. 先計算左邊的表示式(很重要,先計算左邊的!這裡計算的是左邊表示式在記憶體中的地址值)
  2. 在計算右邊的表示式(先左後右,這裡計算的是右邊的值,這裡的值就是值,並不特指地址!若是物件則為地址值)
  3. 計算右邊的值

(等等,這裡有疑問,為什麼運算兩遍右邊?第一遍是計算右邊表示式的(+ — * /)所得到的值,第二遍是返回這個結果值,也就是每個表示式都有返回值!)

  1. 這裡balala~~~
  2. 呼叫函式PutValue,這個做的才是給值,就是將右邊的返回值rval放進左邊的lref對應的地址值(也就是對應的記憶體)
  3. 然後返回這個表示式的值,也就是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} 下面我們們開工吧!


  • 第一、二句程式碼執行之後,記憶體圖如下:

    解析一道JS面試題

  • 第三句程式碼先進行改寫

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} ) )
複製程式碼

如圖:

解析一道JS面試題

解析一道JS面試題


如上圖來進行運算 addr(a.x) = ( addr(a) = value( {n : 2} ) )

  1. 先計算所有等號左邊的值:addr(a.x)的值為:0x8889、addr(a)的值為:0x0001 (0x8889 <-- (0x0001 <-- (0x9999)))
  2. 在計算{n : 2}的值為:0x9999(因為{n : 2}是一個物件,所以在這裡計算得到的是地址值)
  3. 將0x9999作為當前{n : 2}的返回值(也就是當前表示式的右邊的值)
  4. 將返回值賦值給0x0001(對應的記憶體)
  5. 返回( addr(a) = value( {n : 2} ) )的值:0x9999
  6. 將返回值賦值給0x8889(對應記憶體)
  7. 返回addr(a.x) = ( addr(a) = value( {n : 2} ) )的值: 0x9999

一開始找到當前表示式左右左邊的值,也就是他們地址值。然後依次將右值逐個放到對應的記憶體!

最後因為a的地址是指向0x9999的,且其不存x這個屬性,故返回undefined。而b的地址指向裡面存在一個儲存{n:2}的地址的x屬性,故返回的是物件。

相關文章