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


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


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

JavaScript-wat

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

讓我們快速的復習一下.在JavaScript中,一共有兩種類型的值:原始值(primitives)和對象值(objects).原始值有:undefined, null, 布爾值(booleans), 數字(numbers),還有字符串(strings).其他的所有值都是對象類型的值,包括數組(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([])
Number([].toString())  // 因為[].valueOf()不是原始值
Number("")
0

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

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

下面是SpiderMonkey 和 nodejs 中的結果對比.

3.其他

在大多數情況下,想要弄明白JavaScript中的+號是如何工作的並不難:你只能將數字和數字相加或者字符串和字符串相加.對象值會被轉換成原始值后再進行計算.如果你想連接多個數組,需要使用數組的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”(已牆).

4.參考

  1. JavaScript values: not everything is an object


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM