各位小夥伴們好,今天我們來聊一聊JavaScript 中的「型別系統」。
但是在開始之前呢我們可以先思考一個簡單的表示式,那就是在 JavaScript 中,「1+‘2’等於多少?」
其實這相當於是在問,在 JavaScript 中,讓數位和字串相加是會報錯,還是可以正確執行。
如果能正確執行,那麼結果是等於數位 3,還是字串「3」,還是字串「12」呢?
如果你嘗試用一些其他語言執行數位了字串相加,會是什麼楊的結果呢。
比如說用 Python 使用數位和字串進行相加操作,則會直接返回一個執行錯誤,錯誤提示是這樣的:
>>>1+'2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and
'str'
但是在 JavaScript 中執行這段表示式,卻是可以返回一個結果的,最終返回的結果是字串「12」。
那麼為什麼同樣的表示式,在 Python 和 JavaScript 中執行為什麼會有不同的結果?為什麼在 JavaScript 中執行,輸出的是字串「12」,不是數位 3 或者字串「3」呢?
在上邊的表示式中,涉及到了兩種不同型別的資料的相加。要想理清以上兩個問題,我們就需要知道型別的概念,以及 JavaScript 操作型別的策略。
對機器語言來說,所有的資料都是一堆二進位制程式碼,CPU 處理這些資料的時候,並沒有型別的概念,CPU 所做的僅僅是行動資料,比如對其進行移位,相加或相乘。
而在高階語言中,我們都會為操作的資料賦予指定的型別,型別可以確認一個值或者一組值具有特定的意義和目的。所以,型別是高階語言中的概念。
在 JavaScript 中,你可以這樣定義變數:
var num = 100 # 賦值整型變數
let miles = 1000.0 # 浮點型
const name = "John" # 字串
瞭解了型別系統,接下來我們就可以來看看 V8 是怎麼處理 1+「2」的了。當有兩個值相加的時候,比如:
a+b
V8 會嚴格根據 ECMAScript 規範來執行操作。ECMAScript 是一個語言標準,JavaScript 就是 ECMAScript 的一個實現,比如在 ECMAScript 就定義了怎麼執行加法操作,如下所示:
具體細節你也可以參考規範,我將標準定義的內容翻譯如下:
- 把第一個表示式 (AdditiveExpression) 的值賦值給左參照 (lref)。
- 使用 GetValue(lref) 獲取左參照 (lref) 的計算結果,並賦值給左值。
- 使用ReturnIfAbrupt(lval) 如果報錯就返回錯誤。
- 把第二個表示式 (MultiplicativeExpression) 的值賦值給右參照 (rref)。
- 使用 GetValue(rref) 獲取右參照 (rref) 的計算結果,並賦值給 rval。
- 使用ReturnIfAbrupt(rval) 如果報錯就返回錯誤。
- 使用 ToPrimitive(lval) 獲取左值 (lval) 的計算結果,並將其賦值給左原生值 (lprim)。
- 使用 ToPrimitive(rval) 獲取右值 (rval) 的計算結果,並將其賦值給右原生值 (rprim)。
如果 Type(lprim) 和 Type(rprim) 中有一個是 String,則:
a. 把 ToString(lprim) 的結果賦給左字串 (lstr);
b. 把 ToString(rprim) 的結果賦給右字串 (rstr);
c. 返回左字串 (lstr) 和右字串 (rstr) 拼接的字串。
- 把 ToNumber(lprim) 的結果賦給左數位 (lnum)。
- 把 ToNumber(rprim) 的結果賦給右數位 (rnum)。
- 返回左數位 (lnum) 和右數位 (rnum) 相加的數值。
通俗地理解,V8 會提供了一個 ToPrimitive 方法,其作用是將 a 和 b 轉換為原生資料型別,其轉換流程如下:
- 先檢測該物件中是否存在 valueOf 方法,如果有並返回了原始型別,那麼就使用該值進行強制型別轉換;
- 如果 valueOf 沒有返回原始型別,那麼就使用 toString 方法的返回值;
- 如果 vauleOf 和 toString 兩個方法都不返回基本型別值,便會觸發一個 TypeError 的錯誤。
當 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"
你覺得執行這段程式碼會列印出什麼內容呢?歡迎你在留言區與我分享討論。