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

來源:互聯網
上載者:User

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

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

在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)valueOftoStringTypeError: 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() === arrtrue

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

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

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

類似的,Null 物件轉換成字串是這樣的.

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

所以最終的結果是 "" 和 "[object Object]" 兩個字串的串連.

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

> 5 + new Number(7)12> 6 + { valueOf: function () { return 2 } }8> "abc" + { toString: function () { return "def" } }'abcdef'
2.1 意想不到的結果

如果加號前面的第一個運算元是個Null 物件字面量,則結果會出乎我們的意料(下面的代碼在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
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.