JavaScript中,{}+{} 等於多少?

原文:What is {} + {} in JavaScript?javascript

譯者:justjavachtml


最近,Gary Bernhardt 在一個簡短的演講視頻「Wat」中指出了一個有趣的 JavaScript 怪癖: 在把對象和數組混合相加時,會獲得一些意想不到的結果。 本篇文章會依次講解這些計算結果是如何得出的。java

在 JavaScript 中,加法的規則其實很簡單,只有兩種狀況:express

  • 把數字和數字相加數組

  • 把字符串和字符串相加函數

全部其餘類型的值都會被自動轉換成這兩種類型的值。 爲了可以弄明白這種隱式轉換是如何進行的,咱們首先須要搞懂一些基礎知識。this

注意:在下面的文章中提到某一章節的時候(好比§9.1),指的都是 ECMA-262 語言規範(ECMAScript 5.1)中的章節。prototype

讓咱們快速的複習一下。 在 JavaScript 中,一共有兩種類型的值:翻譯

  • 原始值(primitives)code

    1. undefined

    2. null

    3. boolean

    4. number

    5. string

  • 對象值(objects)。

除了原始值外,其餘的全部值都是對象類型的值,包括數組(array)和函數(function)。

類型轉換

加法運算符會觸發三種類型轉換:

  1. 轉換爲原始值

  2. 轉換爲數字

  3. 轉換爲字符串

經過 ToPrimitive() 將值轉換爲原始值

JavaScript 引擎內部的抽象操做 ToPrimitive() 有着這樣的簽名:

ToPrimitive(input,PreferredType?)

可選參數 PreferredType 能夠是 Number 或者 String。 它只表明了一個轉換的偏好,轉換結果不必定必須是這個參數所指的類型(汗),但轉換結果必定是一個原始值。 若是 PreferredType 被標誌爲 Number,則會進行下面的操做來轉換 input (§9.1):

  1. 若是 input 是個原始值,則直接返回它。

  2. 不然,若是 input 是一個對象。則調用 obj.valueOf() 方法。 若是返回值是一個原始值,則返回這個原始值。

  3. 不然,調用 obj.toString() 方法。 若是返回值是一個原始值,則返回這個原始值。

  4. 不然,拋出 TypeError 異常。

若是 PreferredType 被標誌爲 String,則轉換操做的第二步和第三步的順序會調換。 若是沒有 PreferredType 這個參數,則 PreferredType 的值會按照這樣的規則來自動設置:

  • Date 類型的對象會被設置爲 String

  • 其它類型的值會被設置爲 Number

經過 ToNumber() 將值轉換爲數字

下面的表格解釋了 ToNumber() 是如何將原始值轉換成數字的 (§9.3)。

參數 結果
undefined NaN
null +0
boolean true被轉換爲1,false轉換爲+0
number 無需轉換
string 由字符串解析爲數字。例如,"324"被轉換爲324

若是輸入的值是一個對象,則會首先會調用 ToPrimitive(obj, Number) 將該對象轉換爲原始值, 而後在調用 ToNumber() 將這個原始值轉換爲數字。

經過ToString()將值轉換爲字符串

下面的表格解釋了 ToString() 是如何將原始值轉換成字符串的(§9.8)。

參數 結果
undefined "undefined"
null "null"
boolean "true" 或者 "false"
number 數字做爲字符串。好比,"1.765"
string 無需轉換

若是輸入的值是一個對象,則會首先會調用 ToPrimitive(obj, String) 將該對象轉換爲原始值, 而後再調用 ToString() 將這個原始值轉換爲字符串。

實踐一下

下面的對象可讓你看到引擎內部的轉換過程。

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

Number 做爲一個函數被調用(而不是做爲構造函數調用)時,會在引擎內部調用 ToNumber() 操做:

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

加法

有下面這樣的一個加法操做。

value1 + value2

在計算這個表達式時,內部的操做步驟是這樣的 (§11.6.1):

  1. 將兩個操做數轉換爲原始值 (如下是數學表示法的僞代碼,不是能夠運行的 JavaScript 代碼):

    prim1 := ToPrimitive(value1)
    prim2 := ToPrimitive(value2)

    PreferredType 被省略,所以 Date 類型的值採用 String,其餘類型的值採用 Number

  2. 若是 prim1 或者 prim2 中的任意一個爲字符串,則將另一個也轉換成字符串,而後返回兩個字符串鏈接操做後的結果。

  3. 不然,將 prim1 和 prim2 都轉換爲數字類型,返回他們的和。

預料到的結果

當你將兩個數組相加時,結果正是咱們指望的:

> [] + []
''

[] 被轉換成一個原始值:首先嚐試 valueOf() 方法,該方法返回數組自己(this):

> var arr = [];
> arr.valueOf() === arr
true

此時結果不是原始值,因此再調用 toString() 方法,返回一個空字符串(string 是原始值)。 所以,[] + [] 的結果其實是兩個空字符串的鏈接。

將一個數組和一個對象相加,結果依然符合咱們的指望:

> [] + {}
'[object Object]'

解析:將空對象轉換成字符串時,產生以下結果。

> String({})
'[object Object]'

因此最終的結果實際上是把 """[object Object]" 兩個字符串鏈接起來。

更多的對象轉換爲原始值的例子:

> 5 + new Number(7)
12
> 6 + { valueOf: function () { return 2 } }
8
> "abc" + { toString: function () { return "def" } }
'abcdef'

意想不到的結果

若是 + 加法運算的第一個操做數是個空對象字面量,則會出現詭異的結果(Firefox console 中的運行結果):

> {} + {}
NaN

天哪!神馬狀況?(譯註:原文沒有,是我第一次讀到這兒的時候感到太吃驚了,翻譯的時候加入的。) 這個問題的緣由是,JavaScript 把第一個 {} 解釋成了一個空的代碼塊(code block)並忽略了它。 NaN 實際上是表達式 +{} 計算的結果 (+ 加號以及第二個 {})。 你在這裏看到的 + 加號並非二元運算符「加法」,而是一個一元運算符,做用是將它後面的操做數轉換成數字,和 Number() 函數徹底同樣。例如:

> +"3.65"
3.65

如下的表達式是它的等價形式:

+{}
Number({})
Number({}.toString())  // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN

爲何第一個 {} 會被解析成代碼塊(code block)呢? 由於整個輸入被解析成了一個語句:若是左大括號出如今一條語句的開頭,則這個左大括號會被解析成一個代碼塊的開始。 因此,你也能夠經過強制把輸入解析成一個表達式來修復這樣的計算結果: (譯註:咱們期待它是個表達式,結果卻被解析成了語句,表達式和語句的區別能夠查看我之前的『代碼之謎』系列的 語句與表達式。)

> ({} + {})
'[object Object][object Object]'

一個函數或方法的參數也會被解析成一個表達式:

> console.log({} + {})
[object Object][object Object]

通過前面的講解,對於下面這樣的計算結果,你也應該不會感到吃驚了:

> {} + []
0

在解釋一次,上面的輸入被解析成了一個代碼塊後跟一個表達式 +[]。 轉換的步驟是這樣的:

+[]
Number([])
Number([].toString())  // [].valueOf() isn’t primitive
Number("")
0

有趣的是,Node.js 的 REPL 在解析相似的輸入時,與 Firefox 和 Chrome(和Node.js 同樣使用 V8 引擎) 的解析結果不一樣。 下面的輸入會被解析成一個表達式,結果更符合咱們的預料:

> {} + {}
'[object Object][object Object]'
> {} + []
'[object Object]'

3. 這就是全部嗎?

在大多數狀況下,想要弄明白 JavaScript 中的 + 號是如何工做的並不難:你只能將數字和數字相加或者字符串和字符串相加。 對象值會被轉換成原始值後再進行計算。若是將多個數組相加,可能會出現你意料以外的結果,相關文章請參考在 javascript 中,爲何 [1,2] + [3,4] 不等於 [1,2,3,4]?爲何 ++[[]][+[]]+[+[]] = 10?

若是你想鏈接多個數組,須要使用數組的 concat 方法:

> [1, 2].concat([3, 4])
[1, 2, 3, 4]

JavaScript 中沒有內置的方法來「鏈接" (合併)多個對象。 你可使用一個 JavaScript 庫,好比 Underscore:

> var o1 = {eeny:1, meeny:2};
> var o2 = {miny:3, moe: 4};
> _.extend(o1, o2)
{eeny: 1, meeny: 2, miny: 3, moe: 4}

注意:和 Array.prototype.concat() 方法不一樣,extend() 方法會修改它的第一個參數,而不是返回合併後的對象:

> o1
{eeny: 1, meeny: 2, miny: 3, moe: 4}
> o2
{miny: 3, moe: 4}

若是你想了解更多有趣的關於運算符的知識,你能夠閱讀一下 「Fake operator overloading in JavaScript」(中文正在翻譯中)。

參考

  1. JavaScript 並不是全部的東西都是對象

  2. JavaScript:將全部值都轉換成對象

相關文章
相關標籤/搜索