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

原文:http://2ality.com/2012/01/object-plus-object.html

譯文:https://justjavac.com/javascript/2012/12/20/object-plus-object.html  

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

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

  • 把數字和數字相加

  • 把字符串和字符串相加

所有其他類型的值都會被自動轉換成這兩種類型的值。 爲了能夠弄明白這種隱式轉換是如何進行的,我們首先需要搞懂一些基礎知識。

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

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

  • 原始值(primitives)

    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

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

> +"3.65"
3.65

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

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

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

> ({} + {})
'[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:將所有值都轉換成對象

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