JS 中賦值語句的祕密

HT發表於2018-12-28

前言

今天再學習ts的列舉型別的時候,對ts解釋成js後的程式碼有疑問。帶著疑問,一步步追根溯源,最終有了這篇文章。

問題起源

ts的列舉型別解析成js

這是一段簡單的ts程式碼解析成js程式碼的例子。

image.png
左邊是ts程式碼,簡單的宣告一個列舉型別。 右邊是解析出來的js程式碼。

    Direction[Direction["Up"] = 1] = "Up";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
    Direction[Direction["Right"] = 4] = "Right";
複製程式碼

這幾句程式碼,引起了我的注意。 因為,這四句程式碼中,有8個賦值操作。

賦值操作1:Direction["Up"] = 1

賦值操作2:Direction[1] = "Up"

賦值操作3:Direction["Down"] = 2

賦值操作4:Direction[2] = "Down"

賦值操作5:Direction["Left"] = 3

賦值操作6:Direction[3] = "Left"

賦值操作7:Direction["Right"] = 4

賦值操作8:Direction[4] = "Right"

為什麼會有Direction[1] = "Up"這類賦值呢?

經查閱資料發現,原來每個賦值語句都有返回值的(叫返回值可能不太準確,先這麼叫著吧...?)

具體是這樣的:

image.png

可以看到,宣告變數的時候,返回值是undefined, aaa=2的時候,返回值是2.

所以賦值的時候,是有返回值。並且這個返回值是賦值號=右邊的值。如下圖:

image.png

所以,上面的ts解析出來的js程式碼就可以讀懂了。

Direction[Direction["Up"] = 1] = "Up"
// 相當於
Direction["Up"] = 1
Direction[1] = "Up"
複製程式碼

連續賦值問題

有了上面的認識後,引申出了一個新的問題。 連續賦值的時候,第二個賦值的值來源是什麼? 例如:

b = a = 10
複製程式碼

以前的認知(可能是學過C和C++的原因,留下了印象。),賦值語句是從右往左執行的。 上面的語句是預設是 10賦值給a,a賦值給b。很簡單,我們知道a的值是10b的值也是10但是,因為a = 10 有返回值(10),所以b的值是從a來的還是從這個(10)來的呢?

我們來看一個例子:

var a = { name: 'HoTao' }
a.myName = a = { name: '你好' }
console.log(a) // { name: '你好' }
console.log(a.myName) // undefined
複製程式碼

按我們正常的理解,連續賦值語句,從右往左賦值,那麼應該是 a = { name: '你好' }, a.myName = a。所以,輸出的結果應該都是{ name: '你好' },但是,事與願違,並不是這個結果。

這是怎麼回事啊?

於是做了以下測試:

前置知識:JS中,物件和陣列屬於引用型別,在將它們賦值給別人時,傳的是記憶體地址

var a = { name: 'HoTao' }
var b = a
b.name = '你好'
console.log(a.name) // 你好
console.log(b.name) // 你好
複製程式碼

上述程式碼解析:

  • 宣告瞭一個變數a,並且指向了一值為{ name: 'HoTao' }的記憶體地址(a1
  • 宣告瞭一個變數b,並且指向了變數a的地址,即(a1
  • b.name = '你好'這句程式碼,修改了記憶體地址a1所指向的物件的name的值。所以a和b同時受到了影響。

再看一個例子:

var a = { name: 'HoTao' }
var b = a
b = { name: '你好' }
console.log(a.name) // HoTao
console.log(b.name) // 你好
複製程式碼

當執行b= { name: '你好' }給b賦了一個新的記憶體地址(a2),所以,變數a和變數b已經指向不同的地址,他們兩個現在毫無瓜葛了。

我們再反過來看一下連續賦值的問題:

var a = { name: 'HoTao' }
var b = a
a.myName = a = { name: '你好' }
console.log(a)		// { name: '你好'}
console.log(a.myName)	// undefined
console.log(b)		// { name: 'Hotao', myName: { name: '你好' } }
console.log(b.myName)	// { name: '你好' }
複製程式碼

程式碼解析:

  • 宣告瞭兩個變數a、b。都指向值為{ name: 'HoTao' }的記憶體地址(叫a1
  • 執行連續賦值語句,從右往左執行:
    • 先執行 a = { name: '你好' }。這個時候變數a指向的記憶體地址變成了值為{ name: '你好' }的記憶體地址(叫a2),並且這句賦值語句有返回值,返回值為 { name: '你好' }
    • 接著執行 a.myName = { name: '你好' },這個時候a.myName中的a還是指向a1。(因為js是解釋執行的語言,在直譯器對程式碼進行解釋的階段,就已經確定了a.myName的指向)
    • 所以再執行執行a.myName = { name: '你好' }之前,就已經對a.myName再記憶體地址a1所指向的物件中建立了一個屬性名為myName的屬性,預設值就是我們常見的undefined。所以,執行a.myName = { name: '你好' }的結果,就是往記憶體地址為a1所指向的物件中的myName屬性賦值。
    • 輸出結果中a.myNameundefined,是因為此時變數a指向的地址是(a2)a2中沒有myName這個屬性
    • 輸出結果中b.myName{ name: '你好' },是因為b指向的記憶體地址是(a1),而,a1存在myName這個屬性,並且還成功賦值了。所以,正常輸出

總結一下(雖然有點繞~)

  • JS是先解釋,再執行。所有的變數宣告,再解釋階段的時候,就已經宣告瞭這篇文章解釋得很好
  • 當例子中 a.myName = a = { name: '你好' } 時,由於連續賦值語句是從右自左,先執行a = { name: '你好' },執行後 a 在記憶體中的地址已經改變了
  • 執行下一句 a.myName = { name: '你好' } 時,由於解析時已經確定了 a.myName所指向的地址為變數a原來的記憶體地址(a1) ,所以 a.myName = { name: '你好' }是給變數a原來的記憶體地址(a1)指向的變數賦了值。
  • 最後輸出 console.log(a.myName) ,由於 a 現在是指向新地址(a2),而我們只給變數a的舊地址(a1)的 a.myName 賦了值,新地址a2中沒有a.myName這個屬性。

相關文章