原文:What is {} + {} in JavaScript?
译者:justjavac
近来,Gary Bernhardt 在一个简短的演讲视频“Wat”中指出了一个风趣的 JavaScript 怪癖: 在把对象和数组夹杂相加时,会获得一些意想不到的效果。 本篇文章会顺次解说这些盘算效果是怎样得出的。
在 JavaScript 中,加法的划定规矩实在很简单,只要两种状况:
把数字和数字相加
把字符串和字符串相加
一切其他范例的值都会被自动转换成这两种范例的值。 为了能够弄邃晓这类隐式转换是怎样举行的,我们起首须要搞懂一些基础学问。
注重:鄙人面的文章中提到某一章节的时刻(比方§9.1),指的都是 ECMA-262 言语范例(ECMAScript 5.1)中的章节。
让我们疾速的温习一下。 在 JavaScript 中,一共有两种范例的值:
原始值(primitives)
undefined
null
boolean
number
string
对象值(objects)。
除了原始值外,其他的一切值都是对象范例的值,包含数组(array)和函数(function)。
范例转换
加法运算符会触发三种范例转换:
转换为原始值
转换为数字
转换为字符串
经由历程 ToPrimitive() 将值转换为原始值
JavaScript 引擎内部的笼统操纵 ToPrimitive()
有着如许的署名:
ToPrimitive(input,PreferredType?)
可选参数 PreferredType
可所以 Number
或许 String
。 它只代表了一个转换的偏好,转换效果不一定必需是这个参数所指的范例(汗),但转换效果一定是一个原始值。 假如 PreferredType
被标志为 Number
,则会举行下面的操纵来转换 input
(§9.1):
假如
input
是个原始值,则直接返回它。不然,假如
input
是一个对象。则挪用obj.valueOf()
要领。 假如返回值是一个原始值,则返回这个原始值。不然,挪用
obj.toString()
要领。 假如返回值是一个原始值,则返回这个原始值。不然,抛出
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):
将两个操纵数转换为原始值 (以下是数学示意法的伪代码,不是能够运转的 JavaScript 代码):
prim1 := ToPrimitive(value1) prim2 := ToPrimitive(value2)
PreferredType
被省略,因而Date
范例的值采纳String
,其他范例的值采纳Number
。假如 prim1 或许 prim2 中的恣意一个为字符串,则将别的一个也转换成字符串,然后返回两个字符串衔接操纵后的效果。
不然,将 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
天哪!神马状况?(译注:原文没有,是我第一次读到这儿的时刻觉得太受惊了,翻译的时刻到场的。) 这个题目的原因是,JavaScript 把第一个 {}
诠释成了一个空的代码块(code block)并疏忽了它。 NaN
实际上是表达式 +{}
盘算的效果 (+
加号以及第二个 {}
)。 你在这里看到的 +
加号并不是二元运算符「加法」,而是一个一元运算符,作用是将它背面的操纵数转换成数字,和 Number()
函数完整一样。比方:
> +"3.65"
3.65
以下的表达式是它的等价情势:
+{}
Number({})
Number({}.toString()) // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN
为何第一个 {}
会被剖析成代码块(code block)呢? 由于全部输入被剖析成了一个语句:假如左大括号涌现在一条语句的开首,则这个左大括号会被剖析成一个代码块的最先。 所以,你也能够经由历程强迫把输入剖析成一个表达式来修复如许的盘算效果: (译注:我们期待它是个表达式,效果却被剖析成了语句,表达式和语句的区分能够检察我之前的『代码之谜』系列的 语句与表达式。)
> ({} + {})
'[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”(中文正在翻译中)。