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

最近烤蜕,Gary Bernhardt 在一個(gè)簡短的演講視頻“Wat”中指出了一個(gè)有趣的 JavaScript 怪癖: 在把對象和數(shù)組混合相加時(shí)问畅,會(huì)得到一些意想不到的結(jié)果。 本篇文章會(huì)依次講解這些計(jì)算結(jié)果是如何得出的。

在 JavaScript 中,加法的規(guī)則其實(shí)很簡單毅臊,只有兩種情況:

把數(shù)字和數(shù)字相加

把字符串和字符串相加

所有其他類型的值都會(huì)被自動(dòng)轉(zhuǎn)換成這兩種類型的值。 為了能夠弄明白這種隱式轉(zhuǎn)換是如何進(jìn)行的黑界,我們首先需要搞懂一些基礎(chǔ)知識(shí)管嬉。

注意:在下面的文章中提到某一章節(jié)的時(shí)候(比如§9.1),指的都是 ECMA-262 語言規(guī)范(ECMAScript 5.1)中的章節(jié)朗鸠。

讓我們快速的復(fù)習(xí)一下蚯撩。 在 JavaScript 中,一共有兩種類型的值:

原始值(primitives)

undefined

null

boolean

number

string

對象值(objects)烛占。

除了原始值外胎挎,其他的所有值都是對象類型的值,包括數(shù)組(array)和函數(shù)(function)扰楼。

類型轉(zhuǎn)換

加法運(yùn)算符會(huì)觸發(fā)三種類型轉(zhuǎn)換:

轉(zhuǎn)換為原始值

轉(zhuǎn)換為數(shù)字

轉(zhuǎn)換為字符串

通過 ToPrimitive() 將值轉(zhuǎn)換為原始值

JavaScript 引擎內(nèi)部的抽象操作ToPrimitive()有著這樣的簽名:

ToPrimitive(input呀癣,PreferredType?)

可選參數(shù)PreferredType可以是Number或者String。 它只代表了一個(gè)轉(zhuǎn)換的偏好弦赖,轉(zhuǎn)換結(jié)果不一定必須是這個(gè)參數(shù)所指的類型(汗),但轉(zhuǎn)換結(jié)果一定是一個(gè)原始值浦辨。 如果PreferredType被標(biāo)志為Number蹬竖,則會(huì)進(jìn)行下面的操作來轉(zhuǎn)換input(§9.1):

如果input是個(gè)原始值,則直接返回它流酬。

否則币厕,如果input是一個(gè)對象。則調(diào)用obj.valueOf()方法芽腾。 如果返回值是一個(gè)原始值旦装,則返回這個(gè)原始值。

否則摊滔,調(diào)用obj.toString()方法阴绢。 如果返回值是一個(gè)原始值店乐,則返回這個(gè)原始值。

否則呻袭,拋出TypeError異常眨八。

如果PreferredType被標(biāo)志為String,則轉(zhuǎn)換操作的第二步和第三步的順序會(huì)調(diào)換左电。 如果沒有PreferredType這個(gè)參數(shù)廉侧,則PreferredType的值會(huì)按照這樣的規(guī)則來自動(dòng)設(shè)置:

Date類型的對象會(huì)被設(shè)置為String,

其它類型的值會(huì)被設(shè)置為Number篓足。

通過 ToNumber() 將值轉(zhuǎn)換為數(shù)字

下面的表格解釋了ToNumber()是如何將原始值轉(zhuǎn)換成數(shù)字的 (§9.3)段誊。

參數(shù)結(jié)果

undefinedNaN

null+0

booleantrue被轉(zhuǎn)換為1,false轉(zhuǎn)換為+0

number無需轉(zhuǎn)換

string由字符串解析為數(shù)字。例如栈拖,"324"被轉(zhuǎn)換為324

如果輸入的值是一個(gè)對象连舍,則會(huì)首先會(huì)調(diào)用ToPrimitive(obj, Number)將該對象轉(zhuǎn)換為原始值, 然后在調(diào)用ToNumber()將這個(gè)原始值轉(zhuǎn)換為數(shù)字辱魁。

通過ToString()將值轉(zhuǎn)換為字符串

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

參數(shù)結(jié)果

undefined"undefined"

null"null"

boolean"true" 或者 "false"

number數(shù)字作為字符串。比如染簇,"1.765"

string無需轉(zhuǎn)換

如果輸入的值是一個(gè)對象参滴,則會(huì)首先會(huì)調(diào)用ToPrimitive(obj, String)將該對象轉(zhuǎn)換為原始值, 然后再調(diào)用ToString()將這個(gè)原始值轉(zhuǎn)換為字符串锻弓。

實(shí)踐一下

下面的對象可以讓你看到引擎內(nèi)部的轉(zhuǎn)換過程砾赔。

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

Number作為一個(gè)函數(shù)被調(diào)用(而不是作為構(gòu)造函數(shù)調(diào)用)時(shí)青灼,會(huì)在引擎內(nèi)部調(diào)用ToNumber()操作:

> Number(obj)

valueOf

toString

TypeError: Cannot convert object to primitive value

加法

有下面這樣的一個(gè)加法操作暴心。

value1+value2

在計(jì)算這個(gè)表達(dá)式時(shí),內(nèi)部的操作步驟是這樣的 (§11.6.1):

將兩個(gè)操作數(shù)轉(zhuǎn)換為原始值 (以下是數(shù)學(xué)表示法的偽代碼杂拨,不是可以運(yùn)行的 JavaScript 代碼):

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

PreferredType被省略专普,因此Date類型的值采用String,其他類型的值采用Number弹沽。

如果 prim1 或者 prim2 中的任意一個(gè)為字符串檀夹,則將另外一個(gè)也轉(zhuǎn)換成字符串,然后返回兩個(gè)字符串連接操作后的結(jié)果策橘。

否則炸渡,將 prim1 和 prim2 都轉(zhuǎn)換為數(shù)字類型,返回他們的和丽已。

預(yù)料到的結(jié)果

當(dāng)你將兩個(gè)數(shù)組相加時(shí)蚌堵,結(jié)果正是我們期望的:

> [] + []

''

[]被轉(zhuǎn)換成一個(gè)原始值:首先嘗試valueOf()方法,該方法返回?cái)?shù)組本身(this):

> var arr = [];

> arr.valueOf() === arr

true

此時(shí)結(jié)果不是原始值,所以再調(diào)用toString()方法吼畏,返回一個(gè)空字符串(string是原始值)督赤。 因此,[] + []的結(jié)果實(shí)際上是兩個(gè)空字符串的連接宫仗。

將一個(gè)數(shù)組和一個(gè)對象相加够挂,結(jié)果依然符合我們的期望:

> [] + {}

'[object Object]'

解析:將空對象轉(zhuǎn)換成字符串時(shí),產(chǎn)生如下結(jié)果藕夫。

> String({})

'[object Object]'

所以最終的結(jié)果其實(shí)是把""和"[object Object]"兩個(gè)字符串連接起來孽糖。

更多的對象轉(zhuǎn)換為原始值的例子:

> 5 + new Number(7)

12

> 6 + { valueOf: function () { return 2 } }

8

> "abc" + { toString: function () { return "def" } }

'abcdef'

意想不到的結(jié)果

如果+加法運(yùn)算的第一個(gè)操作數(shù)是個(gè)空對象字面量,則會(huì)出現(xiàn)詭異的結(jié)果(Firefox console 中的運(yùn)行結(jié)果):

> {} + {}

NaN

天哪毅贮!神馬情況办悟?(譯注:原文沒有,是我第一次讀到這兒的時(shí)候感到太吃驚了滩褥,翻譯的時(shí)候加入的病蛉。@justjavac) 這個(gè)問題的原因是,JavaScript 把第一個(gè){}解釋成了一個(gè)空的代碼塊(code block)并忽略了它瑰煎。NaN其實(shí)是表達(dá)式+{}計(jì)算的結(jié)果 (+加號(hào)以及第二個(gè){})铺然。 你在?這里看到的+加號(hào)并不是二元運(yùn)算符「加法」,而是一個(gè)一元運(yùn)算符酒甸,作用是將它后面的操作數(shù)轉(zhuǎn)換成數(shù)字魄健,和Number()函數(shù)完全一樣?。例如:

> +"3.65"

3.65

以下的表達(dá)式是它的等價(jià)形式:

+{}

Number({})

Number({}.toString())? // {}.valueOf() isn’t primitive

Number("[object Object]")

NaN

為什么第一個(gè){}會(huì)被解析成代碼塊(code block)呢插勤? 因?yàn)檎麄€(gè)輸入被解析成了一個(gè)語句:如果左大括號(hào)出現(xiàn)在一條語句的開頭沽瘦,則這個(gè)左大括號(hào)會(huì)被解析成一個(gè)代碼塊的開始。 所以农尖,你也可以通過強(qiáng)制把輸入解析成一個(gè)表達(dá)式來修復(fù)這樣的計(jì)算結(jié)果: (譯注:我們期待它是個(gè)表達(dá)式析恋,結(jié)果卻被解析成了語句,表達(dá)式和語句的區(qū)別可以查看我以前的『代碼之謎』系列的語句與表達(dá)式盛卡。@justjavac

> ({} + {})

'[object Object][object Object]'

一個(gè)函數(shù)或方法的參數(shù)也會(huì)被解析成一個(gè)表達(dá)式:

> console.log({} + {})

[object Object][object Object]

經(jīng)過前面的講解助隧,對于下面這樣的計(jì)算結(jié)果,你也應(yīng)該不會(huì)感到吃驚了:

> {} + []

0

在解釋一次滑沧,上面的輸入被解析成了一個(gè)代碼塊后跟一個(gè)表達(dá)式+[]喇颁。 轉(zhuǎn)換的步驟是這樣的:

+[]

Number([])

Number([].toString())? // [].valueOf() isn’t primitive

Number("")

0

有趣的是,Node.js 的 REPL 在解析類似的輸入時(shí)嚎货,與 Firefox 和 Chrome(和Node.js 一樣使用 V8 引擎) 的解析結(jié)果不同。 下面的輸入會(huì)被解析成一個(gè)表達(dá)式蔫浆,結(jié)果更符合我們的預(yù)料:

> {} + {}

'[object Object][object Object]'

> {} + []

'[object Object]'

3. 這就是所有嗎殖属?

在大多數(shù)情況下,想要弄明白 JavaScript 中的+號(hào)是如何工作的并不難:你只能將數(shù)字和數(shù)字相加或者字符串和字符串相加瓦盛。 對象值會(huì)被轉(zhuǎn)換成原始值后再進(jìn)行計(jì)算洗显。如果將多個(gè)數(shù)組相加外潜,可能會(huì)出現(xiàn)你意料之外的結(jié)果,相關(guān)文章請參考在 javascript 中挠唆,為什么 [1,2] + [3,4] 不等于 [1,2,3,4]处窥?為什么 ++[[]][+[]]+[+[]] = 10?玄组。

如果你想連接多個(gè)數(shù)組滔驾,需要使用數(shù)組的 concat 方法:

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

[1, 2, 3, 4]

JavaScript 中沒有內(nèi)置的方法來“連接” (合并)多個(gè)對象。 你可以使用一個(gè) 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()方法會(huì)修改它的第一個(gè)參數(shù),而不是返回合并后的對象:

> o1

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

> o2

{miny: 3, moe: 4}

如果你想了解更多有趣的關(guān)于運(yùn)算符的知識(shí)患膛,你可以閱讀一下 “Fake operator overloading in JavaScript”(中文正在翻譯中)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摊阀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子踪蹬,更是在濱河造成了極大的恐慌胞此,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跃捣,死亡現(xiàn)場離奇詭異漱牵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)枝缔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門布疙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人愿卸,你說我怎么就攤上這事灵临。” “怎么了趴荸?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵儒溉,是天一觀的道長。 經(jīng)常有香客問我发钝,道長顿涣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任酝豪,我火速辦了婚禮涛碑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘孵淘。我一直安慰自己蒲障,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揉阎,像睡著了一般庄撮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毙籽,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天洞斯,我揣著相機(jī)與錄音,去河邊找鬼坑赡。 笑死烙如,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的垮衷。 我是一名探鬼主播厅翔,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼搀突!你這毒婦竟也來了刀闷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對情侶失蹤仰迁,失蹤者是張志新(化名)和其女友劉穎甸昏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徐许,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡施蜜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雌隅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翻默。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恰起,靈堂內(nèi)的尸體忽然破棺而出修械,到底是詐尸還是另有隱情,我是刑警寧澤检盼,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布肯污,位于F島的核電站,受9級(jí)特大地震影響吨枉,放射性物質(zhì)發(fā)生泄漏蹦渣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一貌亭、第九天 我趴在偏房一處隱蔽的房頂上張望柬唯。 院中可真熱鬧,春花似錦圃庭、人聲如沸权逗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斟薇。三九已至,卻和暖如春恕酸,著一層夾襖步出監(jiān)牢的瞬間堪滨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工蕊温, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留袱箱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓义矛,卻偏偏與公主長得像发笔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子凉翻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容