[譯] JavaScript中{}+{}等於多少?

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

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

> [] + []
""

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

> {} + []
0

> {} + {} // chrome瀏覽器和node環境中
"[object Object][object Object]" 

> {} + {} // firefox瀏覽器中
NaN

在JavaScript中,加法的規則其實很簡單,只有兩種情況:你只能把數字和數字相加,或者字符串和字符串相加,所有其他類型的值都會被自動轉換成這兩種類型的值。爲了能夠弄明白這種隱式轉換是如何進行的,我們首先需要搞懂一些基礎知識。注意:在下面的文章中提到某一章節的時候(比如§9.1),指的都是ECMA-262語言規範(ECMAScript 5.1)中的章節。

讓我們快速的複習一下。在JavaScript中,一共有兩種類型的值:原始值(primitives)和對象值(objects)。原始值有:undefined, null,booleans,numbers,strings。(暫不考慮ES6)。其他的所有值都是對象類型的值,包括數組(arrays)和函數(functions)。

1、類型轉換

加法運算符會觸發三種類型轉換,這剛好對應了JavaScript引擎內部的三種抽象操作。

  • 將值轉換爲原始值 => ToPrimitive()
  • 轉換爲數字 => ToNumber()
  • 轉換爲字符串 => ToString()

1.1 通過ToPrimitive()將值轉換爲原始值

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

ToPrimitive(input, PreferredType?)

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

  1. 如果輸入的值已經是個原始值,則直接返回它。
  2. 否則,如果輸入的值是一個對象。則調用該對象的valueOf()方法。如果valueOf()方法的返回值是一個原始值,則返回這個原始值。
  3. 否則,調用這個對象的toString()方法。如果toString()方法的返回值是一個原始值,則返回這個原始值。
  4. 否則,拋出TypeError異常。

如果PreferredType被標誌爲String,則轉換操作的第二步和第三步的順序會調換。如果沒有PreferredType這個參數,則PreferredType的值會按照這樣的規則來自動設置:Date類型的對象會被設置爲String,其它類型的值會被設置爲Number。

1.2 通過ToNumber()將值轉換爲數字

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

參數 結果
undefined NaN
null +0
布爾值 true被轉爲1,false被轉成+0
數字 無需轉換
字符串 解析字符串中的數字。例如,“324”被轉換爲324

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

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

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

參數 結果
undefined “undefined”
null “null”
布爾值 “true” 或者 “false”
數字 數字作爲字符串,比如. “1.765”
字符串 無需轉換

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

1.4 實踐一下

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

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // 沒有返回原始值
    },
    toString: function () {
        console.log("toString");
        return {}; // 沒有返回原始值
    }
}

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

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

2.加法

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

value1 + value2

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

  1. 將兩個操作數轉換爲原始值 (下面是數學表示法,不是JavaScript代碼):

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

    PreferredType被省略,因此Date類型的值採用String,其他類型的值採用Number。

  2. 如果prim1或者prim2中的任意一個爲字符串,則將另外一個也轉換成字符串,然後返回兩個字符串連接操作後的結果。

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

2.1 預料到的結果

兩個空數組相加時,結果是我們所預料的:

> [] + []
""

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

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

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

將一個空數組和一個空對象相加,結果也符合我們的預期:

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

類似的,空對象轉換成字符串是這樣的:

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

所以最終的結果是 """[object Object]" 兩個字符串的連接。

下面是更多的對象轉換爲原始值的例子,你能搞懂嗎:

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

2.1 意想不到的結果

如果加號前面的第一個操作數是個空對象字面量,則結果會出乎我們的意料(下面的代碼在 Firefox 控制檯中運行):

> {} + {}
NaN

這是怎麼回事?原因就是 JavaScript 引擎將第一個 {} 解釋成了一個空的代碼塊並且忽略了它。NaN 其實是後面的表達式 +{} 計算的結果。這裏的加號並不是代表加法的二元運算符,而是一個一元運算符,作用是將後面的操作數轉換成數字,和 Number() 函數完全一樣。例如:

> +"3.65"
3.65

轉換的步驟是這樣的:

+{}
Number({})
Number({}.toString()) // 因爲{}.valueOf()不是原始值
Number("[object Object]")
NaN 

爲什麼第一個 {} 會被解析成代碼塊呢?原因是,整個輸入被解析成了一個語句,如果一個語句是以左大括號開始的,則這個大括號會被解析成一個代碼塊。所以,你也可以通過強制把輸入解析成一個表達式來修復這樣的計算結果:

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

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

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

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

> {} + []
0

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

+[]
Number([].toString()) // 因爲[].valueOf()不是原始值
Number("")
0

有趣的是,在 Node.js(v6.0.0) 和 Chrome(版本 71.0.3578.98) 瀏覽器中下面的輸入會被解析成一個表達式,結果更符合我們的預料:

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

3.其他

在大多數情況下,想要弄明白 JavaScript 中的 + 號是如何工作的並不難:你只能將數字和數字相加或者字符串和字符串相加。對象值會被轉換成原始值後再進行計算。如果你想連接多個數組,需要使用數組的 concat 方法:

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

JavaScript 中沒有內置的方法來“連接" (合併)多個對象。你可以使用一個JavaScript 庫,比如 Underscore: (現在 ES6 中的 Object.assign() 可以實現對象的合併)

> 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”。

4.參考

JavaScript values: not everything is an object

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