{}+{} in Javascript

【译】by lynhao

涉及Javascript专有名词考虑不影响理解均不作翻译

最近,Gary Bernhardt在”Wat”的闪电讲座上指出Javascript的一个有趣的怪癖:当你添加对象或者数组的时候会得到意想不到的结果.这篇文章解释了这些结果

在javascript中添加常用的规则很简单:你只能添加数字和字符串,其他类型的值将会转换成他们其中的一种,为了理解它的转换的工作原理,我们首先需要先了解其他一些事情.每当提到段落(如§9.1)时,它就是指代ECMA-262语言标准(ECMAScript 5.1)。

让我们先从一个快速的复习开始.javascript有两种类型:primitives(基元) and objects(对象),原始类型包括:undefined,null, booleans,numbers,和strings,除此之外其他值都是对象,包括了数组和函数

1.转换值

加号运算符执行三种转换: 把值转换为primitives, numbersstrings

1.1 通过ToPrimitive()将值转化为primitives

ToPrimitive(input, PreferredType?)

可选参数PreferredType是Number 或 String。它只是表示一种
偏好, 结果总是可以是任何原始值。如果PreferredType是Number,则执行以下步骤来转换输入的值(§9.1):

  1. 如果输入是原始类型, 则直接返回它
  2. 否者, 输入的值是一个对象, 则调用obj.valueOf().如果结果返回是原始类型,则返回
  3. 否者,调用obj.toString. 如果结果是一个原始类型,则返回
  4. 否者,抛出一个TypeError异常

如果PreferredType 是String, 2和3步骤交换.如果缺失PreferredType, 那么对于Date的实例设置为String, 其他的值设置为Number

1.2 通过ToNumber()将值转换为数字

  • undefined -> NaN
  • null -> +0
  • boolean -> ( true -> 1, false -> 0 )
  • number -> 不需要转换
  • string -> 解析字符串中的数字. 例如: “324”-> 324 (译者: 除字符串数字之外都转为 “NaN”)

1.3通过ToString()将值转换为字符串

  • undefined -> “undefined”
  • null -> + “null”
  • oolean -> ( true -> “true” , false -> “false” )
  • number -> 解析成字符串数字
  • string -> 不需要转换

1.4 小试牛刀

下面object允许您观察转换过程

1
2
3
4
5
6
7
8
9
10
var obj = {
valueOf: function () {
console.log("valueOf");
return {}; // 不是原始类型
},
toString: function () {
console.log("toString");
return {}; // 不是原始类型
}
}

Number作为函数调用(跟构造函数相反)内部调用ToNumber():

1
2
3
4
> Number(obj)
valueOf
toString
typeError: 不能将object转换成原始类型的值

2. 加法

给出下面的加法

1
value1 + value2

为了分析上面的表达式, 采取以下步骤(§11.6.1):

  1. 将两个操作符转换为primitives (数学表示法,而不是JavaScript
    1
    2
    prim1:= ToPrimitive(value1)
    rim2:= ToPrimitive(value2)
> PreferredType 被忽略因此Number为non-dates,String为dates
  1. 如果prim1或prim2是一个字符串,则将其转换为字符串并返回连接的结果
  2. 否者,将prim1和prim2都转换成数字并返回他们的总和

2.1 预期结果

添加两个数组,一切按预期运行

1
2
> [] + []
''

首先将[]转换为primitives尝试返回数组本身(this)的valueOf():

1
2
3
> var arr = [];
> arr.valueOf() === arr
true

因为返回的结果不是原始类型,紧接着调用toString()返回空字符串(原始类型的值).因此[]+[]返回是两个空字符串联后的结果

添加数组和对象也符合我们的期望:

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

补充说明: 将空对象转换成字符串会得到下面的结果

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

之前的结果是””和 ‘[object Object]’ 串联后的返回的。

更多对象转换为primitives的示例:

1
2
3
4
5
6
> 5 + new Number(7)
12
> 6 + {valueOf:function(){return 2}}
8
>“abc”+ {toString:function(){return“def”}}
'abcdef'

2.2. 意外的结果

如果+的第一个操作符是一个空的对象字面量(如在FireFox的控制台看到的结果)情况就会变得很诡异

1
2
> {} + {}
NaN

这里发生了什么?问题在于javascript将第一个{}解析成空的
代码块并忽略它.NaN是通过判断+{}(加号后跟第二个{})来计算的.
你在这里看到的加号不是二进制的加法运算符而是一个
一元前缀运算符,它用与Number()相同方式将其操作符转换
成数字.例如:

1
2
> +“3.65”
3.65

下面的表达式都是等效的:

1
2
3
4
5
+{}
Number({})
Number({}.toString()) // {}.valueOf() 不是原始类型的值
Number("[object Object]")
NaN

为什么第一个{}被解析成代码块?因为完整的输入值被解析成语句,并且在语句的
花括号被解析成开始代码块.因此,你可以通过强制将输入值解析为表达式来解决问题:

1
2
>({} + {})
'[object Object] [object Object]'

function或者method的参数也总是被解析成表达式

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

前面解释之后,你应该不再对以下结果感到好奇了:

1
2
> {} + []
0

再分析一次, 被解析成代码块后跟着+[],以下表达式是等效的:

1
2
3
4
5
+[]
Number([])
Number([].toString()) // [].valueOf() 不是原始类型的值
Number("")
0

有趣的是,Node.js REPL分析其输入值与FirefoxChrome不同(它甚至使用与Node.js相同的V8 JavaScript引擎.以下的输入值被解析成表达式结果并没有那么出乎意料

1
2
3
4
> {} + {}
'[object Object][object Object]'
> {} + []
'[object Object]'

这样做的好处是更像使用输入值作为console.log()的参数时获得的结果。但它也不像使用输入值作为程序中的语句。

3.这是什么意思呢?

在大多数情况下,理解+在javascript运行机制并没有那么难.你只能添加数字或者字符串.

对象被转换为字符串(如果另一个操作符是字符串)或数字(否则),如果你想合并数组,你需要用一个方法:

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

Javascript中没有内置的方式去”拼接”(合并)对象.你需要使用库如Underscore:

1
2
3
4
5
6
7
> 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()修改它的第一个参数:

1
2
3
4
5
6
7
> 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

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


感谢您的阅读,本文由 lynhao 原创提供。如若转载,请注明出处:lynhao(http://www.lynhao.cn
事件委托之一二事
原型链深入透彻全面释疑及应用