瞭解V8(二)類型轉換:V8是怎麼實現1+「2」的?

各位小夥伴們好,今天咱們來聊一聊JavaScript 中的「類型系統」。函數

可是在開始以前呢咱們能夠先思考一個簡單的表達式,那就是在 JavaScript 中,「1+‘2’等於多少?」spa

其實這至關因而在問,在 JavaScript 中,讓數字字符串相加是會報錯,仍是能夠正確執行。翻譯

若是能正確執行,那麼結果是等於數字 3,仍是字符串「3」,仍是字符串「12」呢?code

若是你嘗試用一些其餘語言執行數字了字符串相加,會是什麼楊的結果呢。對象

好比說用 Python 使用數字和字符串進行相加操做,則會直接返回一個執行錯誤,錯誤提示是這樣的:ip

>>>1+'2'
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
  TypeError: unsupported operand type(s) for +: 'int' and 
  'str'

可是在 JavaScript 中執行這段表達式,倒是能夠返回一個結果的,最終返回的結果是字符串「12」。rem

那麼爲何一樣的表達式,在 Python 和 JavaScript 中執行爲何會有不一樣的結果?爲何在 JavaScript 中執行,輸出的是字符串「12」,不是數字 3 或者字符串「3」呢?

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

在上邊的表達式中,涉及到了兩種不一樣類型的數據的相加。要想理清以上兩個問題,咱們就須要知道類型的概念,以及 JavaScript 操做類型的策略。字符串

對機器語言來講,全部的數據都是一堆二進制代碼,CPU 處理這些數據的時候,並無類型的概念,CPU 所作的僅僅是移動數據,好比對其進行移位,相加或相乘。虛擬機

而在高級語言中,咱們都會爲操做的數據賦予指定的類型,類型能夠確認一個值或者一組值具備特定的意義和目的。因此,類型是高級語言中的概念it

image.png

在 JavaScript 中,你能夠這樣定義變量:

var num = 100 # 賦值整型變量
  let miles = 1000.0 # 浮點型
  const name = "John" # 字符串

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

瞭解了類型系統,接下來咱們就能夠來看看 V8 是怎麼處理 1+「2」的了。當有兩個值相加的時候,好比:

a+b

V8 會嚴格根據 ECMAScript 規範來執行操做。ECMAScript 是一個語言標準,JavaScript 就是 ECMAScript 的一個實現,好比在 ECMAScript 就定義了怎麼執行加法操做,以下所示:

image.png
具體細節你也能夠參考規範,我將標準定義的內容翻譯以下:

  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,則:

    a. 把 ToString(lprim) 的結果賦給左字符串 (lstr);

    b. 把 ToString(rprim) 的結果賦給右字符串 (rstr);

    c. 返回左字符串 (lstr) 和右字符串 (rstr) 拼接的字符串。

  10. 把 ToNumber(lprim) 的結果賦給左數字 (lnum)。
  11. 把 ToNumber(rprim) 的結果賦給右數字 (rnum)。
  12. 返回左數字 (lnum) 和右數字 (rnum) 相加的數值。

通俗地理解,V8 會提供了一個 ToPrimitive 方法,其做用是將 a 和 b 轉換爲原生數據類型,其轉換流程以下:

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

image.png

當 V8 執行 1+「2」時,由於這是兩個原始值相加,原始值相加的時候,若是其中一項是字符串,那麼 V8 會默認將另一個值也轉換爲字符串,至關於執行了下面的操做:Number(1).toString() + "2"

這裏,把數字 1 偷偷轉換爲字符串「1」的過程也稱爲強制類型轉換,由於這種轉換是隱式的,因此若是咱們不熟悉語義,那麼就很容易判斷錯誤。

咱們還能夠再看一個例子來驗證上面流程,你能夠看下面的代碼:

var Obj = {
      toString() {
        return '200'
      }, 
      valueOf() {
        return 100
      }   
    }
    Obj+3

執行這段代碼,你以爲應該返回什麼內容呢?

上面咱們介紹過了,因爲須要先使用 ToPrimitive 方法將 Obj 轉換爲原生類型,而 ToPrimitive 會優先調用對象中的 valueOf 方法,因爲 valueOf 返回了 100,那麼 Obj 就會被轉換爲數字 100,那麼數字 100 加數字 3,那麼結果固然是 103 了。

若是我改造下代碼,讓 valueOf 方法和 toString 方法都返回對象,其改造後的代碼以下:

var Obj = {
      toString() {
        return new Object()
      }, 
      valueOf() {
        return new Object()
      }   
  }
  Obj+3

再執行這段代碼,你以爲應該返回什麼內容呢?

由於 ToPrimitive 會先調用 valueOf 方法,發現返回的是一個對象,並非原生類型,當 ToPrimitive 繼續調用 toString 方法時,發現 toString 返回的也是一個對象,都是對象,就沒法執行相加運算了,這時候虛擬機就會拋出一個異常,異常以下所示:

VM263:9 Uncaught TypeError: Cannot convert object to primitive value
    at <anonymous>:9:6

提示的是類型錯誤,錯誤緣由是沒法將對象類型轉換爲原生類型。

因此說,在執行加法操做的時候,V8 會經過 ToPrimitive 方法將對象類型轉換爲原生類型,最後就是兩個原生類型相加,若是其中一個值的類型是字符串時,則另外一個值也須要強制轉換爲字符串,而後作字符串的鏈接運算。在其餘狀況時,全部的值都會轉換爲數字類型值,而後作數字的相加。

總結

今天咱們主要了解了 JavaScript 中的類型系統是怎麼工做的。類型系統定義了語言應當如何操做類型,以及這些類型如何互相做用。

在 JavaScript 中,數字和字符串相加會返回一個新的字符串,這是由於 JavaScript 認爲字符串和數字相加是有意義的,V8 會將其中的數字轉換爲字符,而後執行兩個字符串的相加操做,最終獲得的是一個新的字符串。

在 JavaScript 中,類型系統是依據 ECMAScript 標準來實現的,因此 V8 會嚴格根據 ECMAScript 標準來執行。

在執行加法過程當中,V8 會先經過 ToPrimitive 函數,將對象轉換爲原生的字符串或者是數字類型,在轉換過程當中,ToPrimitive 會先調用對象的 valueOf 方法,若是沒有 valueOf 方法,則調用 toString 方法,若是 vauleOf 和 toString 兩個方法都不返回基本類型值,便會觸發一個 TypeError 的錯誤。

思考題

咱們一塊兒來分析一段代碼:

var Obj = {
    toString() {
      return "200"
    }, 
    valueOf() {
      return 100
    }   
  }
  Obj+"3"

你以爲執行這段代碼會打印出什麼內容呢?歡迎你在留言區與我分享討論。

相關文章
相關標籤/搜索