JavaScript 中神奇的加法(隱式類型轉換)

一般來說, 加減法應該是我們學習生涯中接觸到的第一個運算符,通常意義下它也是最簡單的運算符。

在程序語言中,加減法的情況一般也比較簡單,但是在 JavaScript 中加法的情況卻比較奇怪,因爲它有着大量特殊的情況。

我們舉個簡單的例子:

1 + '1' = '11'; 
1 + 'a' = '1a';
1 + []  = '';

從基礎數據類型的加法開始,我們得到的結果就變的奇怪了起來。究其根由,其實是 JavaScript 的隱式轉換在做怪。

一.什麼是 JavaScript 隱式轉換?

在講隱式轉換之前,咱們先得回顧一下 JavaScript5 種基礎數據類型,3種引用數據類型和 3 個特殊值:

基礎數據類型:

  1. number 類型
  2. string 類型
  3. boolean 類型
  4. null 類型
  5. undefined 類型

引用數據類型:

  1. object
  2. function
  3. array

特殊值:

  1. NaN
  2. +Infinity 和 -Infinity
  3. +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:

  1. 如果 input 是個原始值,則直接返回它。 否則,如果 input 是一個對象。則調用 obj.valueOf() 方法。
  2. 如果返回值是一個原始值,則返回這個原始值。 否則,調用 obj.toString() 方法。 如果返回值是一個原始值,則返回這個原始值。
  3. 否則,拋出 TypeError異常。如果PreferredType被標誌爲String,則轉換操作的第二步和第三步的順序會調換。

如果沒有 PreferredType 這個參數,則PreferredType的值會按照這樣的規則來自動設置:

  1. Date 類型的對象會被設置爲 String
  2. 其它類型的值會被設置爲 Number

OK,現在我們知道了 原始數據類型轉換的大致工作流程,我們來看看在一個 加法 中會發生什麼?

有兩個參數 value1value2,有以下算式:

value1 + value2

這時在 JavaScript 引擎內部會發生以下這三步:

  1. 通過 ToPrimitivevalue1value2 轉化爲原始值,此時 PreferredType參數是被忽略的,所以除了 Date 類型外其他類型都會按照 Number 參數來處理。
  2. 此時如果 value1value2 中有一個 string 類型,調用 ToString 方法將另一個參數轉化爲 string 類型並進行字符串的拼接。
  3. 如果 value1value2 中一個 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 的執行上下文中,{}有以下三種含義:

  1. 語句塊
  2. 函數
  3. 對象字面量

而其中 語句塊 這個含義的優先級是最高的

所以,以下這段代碼中的{}相當於全局環境中的 label 語句了:

{} + 1 

實際上等於:

{};+1 

即:

+1 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章