天天看點

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

說明

圖解 Google V8 學習筆記。

什麼是類型系統 (Type System)?

為什麼數字 1 加上字元串 2 輸出的結果是字元串 12 ?

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

要搞清上面這個問題,需要知道類型的概念,以及 JavaScript 操作類型的政策。

對機器語言來說,所有的資料都是一堆二進制代碼,CPU 處理這些資料的時候,并沒有類型的概念,CPU 所做的僅僅是移動資料,比如對其進行移位,相加或相乘。

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

在進階語言中,我們都會為操作的資料賦予指定的類型,類型可以确認一個值或者一組值具有特定的意義和目的。

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

通用的類型有數字類型、字元串、Boolean 類型等等,引入了這些類型之後,編譯器或者解釋器就可以根據類型來限制一些有害的或者沒有意義的操作。

每種語言都定義了自己的類型,還定義了如何操作這些類型,另外還定義了這些類型應該如何互相作用,我們就把這稱為類型系統。

​​wiki 百科:類型系統(type system)​​

在計算機科學中,類型系統(type system)用于定義如何将程式設計語言中的數值和表達式歸類為許多不同的類型,如何操作這些類型,這些類型如何互相作用。

V8 是怎麼執行加法操作的?

V8 會嚴格根據 ECMAScript 語言标準規範來執行操作。

​​ECMAScript定義加法語義​​

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

翻譯如下:

  1. 把第一個表達式​

    ​(AdditiveExpression)​

    ​ 的值指派給左引用 (lref)。
  2. 使用​

    ​GetValue(lref)​

    ​ 擷取左引用 (lref) 的計算結果,并指派給左值。
  3. 使用​

    ​ReturnIfAbrupt(lval)​

    ​ 如果報錯就傳回錯誤。
  4. 把第二個表達式​

    ​(MultiplicativeExpression)​

    ​ 的值指派給右引用 (rref)。
  5. 使用​

    ​GetValue(rref)​

    ​ 擷取右引用 (rref) 的計算結果,并指派給 rval。
  6. 使用​

    ​ReturnIfAbrupt(rval)​

    ​ 如果報錯就傳回錯誤。
  7. 使用​

    ​ToPrimitive(lval)​

    ​ 擷取左值 (lval) 的計算結果,并将其指派給左原生值 (lprim)。
  8. 使用​

    ​ToPrimitive(rval)​

    ​ 擷取右值 (rval) 的計算結果,并将其指派給右原生值 (rprim)。
  9. 如果​

    ​Type(lprim)​

    ​​ 和​

    ​Type(rprim)​

    ​ 中有一個是 String,則:
  • 把​

    ​ToString(lprim)​

    ​ 的結果賦給左字元串 (lstr);
  • 把 `ToString(rprim) 的結果賦給右字元串 (rstr);
  • 傳回左字元串 (lstr) 和右字元串 (rstr) 拼接的字元串。
  1. 把 `ToNumber(lprim) 的結果賦給左數字 (lnum)。
  2. 把 ToNumber(rprim) 的結果賦給右數字 (rnum)。
  3. 傳回左數字 (lnum) 和右數字 (rnum) 相加的數值。

裡面涉及的 ​​ReturnIfAbrupt​​:

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

V8 會提供了一個 ​

​ToPrimitive​

​ 方法,其作用是将 a 和 b 轉換為原生資料類型,其轉換流程如下:

  1. 先檢測該對象中是否存在 valueOf 方法,如果有并傳回了原始類型,那麼就使用該值進行強制類型轉換;
  2. 如果 valueOf 沒有傳回原始類型,那麼就使用 toString 方法的傳回值;
  3. 如果 vauleOf 和 toString 兩個方法都不傳回基本類型值,便會觸發一個 TypeError 的錯誤。

将對象轉換為原生類型的流程圖:

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

兩個原生類型相加:

  1. 如果其中一個值的類型是字元串時,則另一個值也需要強制轉換為字元串,然後做字元串的連接配接運算。
  2. 在其他情況時,所有的值都會轉換為數字類型值,然後做數字的相加。

例子1:

var kaimo = {
    toString() {
      return "777"
    }, 
    valueOf() {
      return 666
    }   
}
kaimo + 1      

先使用 ToPrimitive 方法将 kaimo 轉換為原生類型,ToPrimitive 會優先調用對象中的 valueOf 方法,傳回了 666 Number 類型。

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

例子2:

var kaimo = {
    toString() {
      return "777"
    }, 
    valueOf() {
      return 666
    }   
}
kaimo + "1"      

先使用 ToPrimitive 方法将 kaimo 轉換為原生類型,ToPrimitive 會優先調用對象中的 valueOf 方法,傳回了 666 Number 類型。其中一個值 “1” 的類型是字元串,則另一個值也需要強制轉換為字元串,然後做字元串的連接配接運算。

圖解 Google V8 # 08:類型轉換:V8是怎麼實作 1 + “2” 的?

例子3:

var kaimo = {
    toString() {
      return new Object()
    }, 
    valueOf() {
      return new Object()
    }   
}
kaimo + 1      

因為 ToPrimitive 會先調用 valueOf 方法,發現傳回的是一個對象,并不是原生類型,當 ToPrimitive 繼續調用 toString 方法時,發現 toString 傳回的也是一個對象,都是對象,就無法執行相加運算,這時候虛拟機就會抛出一個異常:​

​Uncaught TypeError: Cannot convert object to primitive value​

​。

ToPrimitive 拓展

參考資料

  • ​​https://262.ecma-international.org/6.0/#sec-addition-operator-plus-runtime-semantics-evaluation​​

繼續閱讀