一般來說, 加減法應該是我們學習生涯中接觸到的第一個運算符,通常意義下它也是最簡單的運算符。
在程序語言中,加減法的情況一般也比較簡單,但是在 JavaScript 中加法的情況卻比較奇怪,因爲它有着大量特殊的情況。
我們舉個簡單的例子:
1 + '1' = '11';
1 + 'a' = '1a';
1 + [] = '';
從基礎數據類型的加法開始,我們得到的結果就變的奇怪了起來。究其根由,其實是 JavaScript 的隱式轉換在做怪。
一.什麼是 JavaScript 隱式轉換?
在講隱式轉換之前,咱們先得回顧一下 JavaScript 的 5 種基礎數據類型,3種引用數據類型和 3 個特殊值:
基礎數據類型:
- number 類型
- string 類型
- boolean 類型
- null 類型
- undefined 類型
引用數據類型:
- object
- function
- array
特殊值:
- NaN
- +Infinity 和 -Infinity
- +0 和 -0
ok,我們回到隱式轉換中,正如我開始舉的那個例子一樣,JavaScript 的隱式轉換總是發生在各種運算符以及特殊的真值判斷中(比如說 if),對與 java 來說,一個 Number 類型值 + 一個 Boolean 類型值 是肯定會報錯的,但是在 JavaScript 中卻不會,因爲隱式轉換會將 Boolean 類型的值轉換爲 Number 值。
二. JavaScript 的加法中隱式轉換是怎麼工作的?
你在百度上搜索 JavaScript 加法特性,你也許會看到這種圖解:
或者是這種:
他們是很有用處的內容總結,但是光靠它們並不能幫我們更好的去理解,所以我們需要實例來支撐理論。
我們先來看看一個有趣的例子:
// number + ?
1 + 1 = 2; // number
1 + '1' = "11"; // string
1 + true = 2; // number
1 + null = 1; //number
1 + undefined = NaN // NaN
1 + {} = "1[object Object]" // string
1 + [] = "1"; // string
// string + ?
'1' + 1 = "11"; // string
'1' + '1' = "11"; // string
'1' + true = "1true"; // string
'1' + null = "1null"; // string
'1' + undefined = "1undefined"; // string
'1' + {} = "1[object object]"; // string
'1' + [] = "1"; // string
看到這大家是不是有點暈?我之所以舉了這兩個例子,是因爲在 JavaScript 的加法中其實只有兩種規則:一種是 number + number,另一種是 string + string。
我們從 string + ?
的例子中很容易的可以得出一個結論,只要在某一個加法中,某一方是字符串,那麼最後的結果一定是字符串。
但是這個結論只能解決我們很小的一部分疑惑,你可能存在類似下面的問題:
number + boolean = ?
number + null = ?
number + undefined = ?
number + object = ?
我覺得,想要理解這其中的轉換,先得理解的是在一個加法運算中,隱式轉換的潛規則到底是什麼。
首先,在 JavaScript 的加法中,會發生三種轉換:
1. 原始類型轉換 ToPrimitive
2. 數字類型轉換 ToNumber
3. 字符串類型轉換 ToString
而轉換的順序永遠是 ToPrimitive
也就是原始類型轉換優先。
那麼什麼是 原始類型轉換?
我們可以把原始類型轉換分爲兩種,第一種是簡單基本類型之間互轉,比如:
number -> string
string -> number
null -> number
...
這其中的轉換沒什麼要注意的地方,大家把我在上面借用過來的圖裏的內容記下來就行。
關鍵點在於第二種,複雜基本類型到簡單基本類型的轉換:
object -> number
object -> string
array -> number
array -> string
function -> number
function -> string
okok,有了上面的基礎,我們再來講講原始類型轉換的工作原理,老規矩,我們打開ECMAScript的官網ecmascript 5.1規範,找到 9.1 節,裏面就有關於 ~ToPrimitive
我給大家放下原版內容:
大概解釋如下:
ToPrimitive
接受一個值 input
,和一個可選的期望類型 PreferredType
作參數。
可選參數 PreferredType
可以是 Number
或者 String
。
如果對象有能力被轉換爲不止一種原語類型,可以使用可選的 PreferredType 類型 來暗示那個類型,但轉換結果一定是一個原始值。
如果 PreferredType 被標誌爲 Number,則會進行下面的操作來轉換input:
- 如果 input 是個原始值,則直接返回它。 否則,如果 input 是一個對象。則調用 obj.valueOf() 方法。
- 如果返回值是一個原始值,則返回這個原始值。 否則,調用 obj.toString() 方法。 如果返回值是一個原始值,則返回這個原始值。
- 否則,拋出 TypeError異常。如果PreferredType被標誌爲String,則轉換操作的第二步和第三步的順序會調換。
如果沒有 PreferredType 這個參數,則PreferredType的值會按照這樣的規則來自動設置:
- Date 類型的對象會被設置爲 String
- 其它類型的值會被設置爲 Number
OK,現在我們知道了 原始數據類型轉換的大致工作流程,我們來看看在一個 加法 中會發生什麼?
有兩個參數 value1
和 value2
,有以下算式:
value1 + value2
這時在 JavaScript 引擎內部會發生以下這三步:
- 通過
ToPrimitive
將value1
和value2
轉化爲原始值,此時PreferredType
參數是被忽略的,所以除了 Date 類型外其他類型都會按照 Number 參數來處理。 - 此時如果
value1
和value2
中有一個string
類型,調用ToString
方法將另一個參數轉化爲string
類型並進行字符串的拼接。 - 如果
value1
和value2
中一個string
類型都沒有,那麼則通過ToNumber
方法將兩個參數都轉化爲number
類型。
我舉幾個例子幫助大家理解下:
1 + '2' = '12' ;
1被 ToPrimitive
返回其自身的原始值 number
, '2’的原始值爲 string
,命中條件2,所以1被 ToString
轉化爲 ‘1’, ‘1’ 與 ‘2’ 進行拼接後得到 ‘12’;
再看一個例子:
1 + [] = '1';
1被 ToPrimitive
返回其自身的原始值 number
, [] 則不太一樣,首先它會調用 valueOf()函數[].valueOf() === []
得到 array
,這並不是原始值,所以它會進行 toString()
轉換,得到空字符串 ""
。然後""
與 1 相加顯然就是拼接了,最終得到'1'
;
再來一個:
1 + {} = 1;
1被 ToPrimitive
返回其自身的原始值 number
,{} 與 [] 又不太一樣,{} 爲對象,它會進行ToPrimitive({}, string)
操作,此時它與 Number
類型中的步驟2,3是相反的,也就是說它會直接調用 toString()
方法,得到 "[object object]"
,類型爲 string
,此時又命中條件2,1被轉化爲'1'
,進行字符串拼接後得到"1[object object]"
。
現在是不是清晰多了?整個工作原理,我們可以把它分爲三部分:
1.簡單基本類型之間的轉化規則:
2.原始基本類型的轉化原則。
3.加法中的執行順序。
三. 害人的的語句優先
有這麼一種情況,會讓人疑惑:
在瀏覽器控制檯輸入以下代碼:
{} + 1 // return 1 number;
1 + {} //return '1[object object]' string
如圖:
還有更讓人疑惑的:
{} + 1 // return 1 number;
console.log({} + 1) // logs '1[object object]' string
如圖:
這一切的根源就在於 {}
的特殊表現,實際上在 JavaScript 的執行上下文中,{}
有以下三種含義:
- 語句塊
- 函數
- 對象字面量
而其中 語句塊 這個含義的優先級是最高的。
所以,以下這段代碼中的{}
相當於全局環境中的 label
語句了:
{} + 1
實際上等於:
{};+1
即:
+1