最近,Gary Bernhardt 在一个简短的演讲视频“Wat”中指出了一个有趣的 JavaScript 怪癖: 在把对象和数组混合相加时,会得到一些意想不到的结果。 本篇文章会依次讲解这些计算结果是如何得出的。

在 JavaScript 中,加法的规则其实很简单,只有两种情况:

  • 把数字和数字相加

  • 把字符串和字符串相加

所有其他类型的值都会被自动转换成这两种类型的值。 为了能够弄明白这种隐式转换是如何进行的,我们首先需要搞懂一些基础知识。

注意:在下面的文章中提到某一章节的时候(比如§9.1),指的都是 ECMA-262 语言规范(ECMAScript 5.1)中的章节。

让我们快速的复习一下。 在 JavaScript 中,一共有两种类型的值:

  • 原始值(primitives)

    1. undefined

    2. null

    3. boolean

    4. number

    5. string

  • 对象值(objects)。

除了原始值外,其他的所有值都是对象类型的值,包括数组(array)和函数(function)。

类型转换

加法运算符会触发三种类型转换:

  1. 转换为原始值

  2. 转换为数字

  3. 转换为字符串

通过 ToPrimitive() 将值转换为原始值

JavaScript 引擎内部的抽象操作 ToPrimitive() 有着这样的签名:

ToPrimitive(input,PreferredType?)

可选参数 PreferredType 可以是 Number 或者 String。 它只代表了一个转换的偏好,转换结果不一定必须是这个参数所指的类型(汗),但转换结果一定是一个原始值。 如果 PreferredType 被标志为 Number,则会进行下面的操作来转换 input (§9.1):

  1. 如果 input 是个原始值,则直接返回它。

  2. 否则,如果 input 是一个对象。则调用 obj.valueOf() 方法。 如果返回值是一个原始值,则返回这个原始值。

  3. 否则,调用 obj.toString() 方法。 如果返回值是一个原始值,则返回这个原始值。

  4. 否则,抛出 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):

  1. 将两个操作数转换为原始值 (以下是数学表示法的伪代码,不是可以运行的 JavaScript 代码):

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

    PreferredType 被省略,因此 Date 类型的值采用 String,其他类型的值采用 Number

  2. 如果 prim1 或者 prim2 中的任意一个为字符串,则将另外一个也转换成字符串,然后返回两个字符串连接操作后的结果。

  3. 否则,将 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

天哪!神马情况?(译注:原文没有,是我第一次读到这儿的时候感到太吃惊了,翻译的时候加入的。@justjavac) 这个问题的原因是,JavaScript 把第一个 {} 解释成了一个空的代码块(code block)并忽略了它。 NaN 其实是表达式 +{} 计算的结果 (+ 加号以及第二个 {})。 你在这里看到的 + 加号并不是二元运算符「加法」,而是一个一元运算符,作用是将它后面的操作数转换成数字,和 Number() 函数完全一样。例如:

> +"3.65"
3.65

以下的表达式是它的等价形式:

+{}
Number({})
Number({}.toString()) // {}.valueOf() isn’t primitive
Number("[object Object]")
NaN

为什么第一个 {} 会被解析成代码块(code block)呢? 因为整个输入被解析成了一个语句:如果左大括号出现在一条语句的开头,则这个左大括号会被解析成一个代码块的开始。 所以,你也可以通过强制把输入解析成一个表达式来修复这样的计算结果: (译注:我们期待它是个表达式,结果却被解析成了语句,表达式和语句的区别可以查看我以前的『代码之谜』系列的 语句与表达式@justjavac

> ({} + {})
'[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”(中文正在翻译中)。

JavaScript中,{}+{}等于多少?的更多相关文章

  1. 【转】JavaScript中,{}+{}等于多少?

    原文链接:http://www.2ality.com/2012/01/object-plus-object.html 译文链接:http://www.cnblogs.com/ziyunfei/arch ...

  2. [译]JavaScript中,{}+{}等于多少?

    最近,Gary Bernhardt在一个简短的演讲视频“Wat”中指出了一个有趣的JavaScript怪癖:在把对象和数组混合相加时,会得到一些你意想不到的结果.本篇文章会依次讲解这些计算结果是如何得 ...

  3. 如何解决JavaScript中0.1+0.2不等于0.3

    console.log(0.1+0.2===0.3)// true or false?? 在正常的数学逻辑思维中,0.1+0.2=0.3这个逻辑是正确的,但是在JavaScript中0.1+0.2!= ...

  4. 在 javascript 中,为什么 [1,2] + [3,4] 不等于 [1,2,3,4]?

    在 stackoverflow 上有人提问:arrays - Why does [1,2] + [3,4] = "1,23,4" in JavaScript? 问题 我想将一个数组 ...

  5. Javascript 中 switch case 等于 (== )还是 恒等于(===)?

    Javascript 中 switch case 等于 (== )还是 恒等于(===)? 可以测试一下以下代码,这个 case 中是 等于(==)还是恒等于(===) <script> ...

  6. 为什么 JavaScript 中 0.1+0.2 不等于 0.3 ?

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/2kea7-jACCJmSYBQAwXyIg作者:刘洋 在 js 中进行数学的运算时,会出现0. ...

  7. JavaScript 中的数据类型

    Javascript中的数据类型有以下几种情况: 基本类型:string,number,boolean 特殊类型:undefined,null 引用类型:Object,Function,Date,Ar ...

  8. JavaScript中‘this’关键词的优雅解释

    本文转载自:众成翻译 译者:MinweiShen 链接:http://www.zcfy.cc/article/901 原文:https://rainsoft.io/gentle-explanation ...

  9. JavaScript中typeof、toString、instanceof、constructor与in

    JavaScript 是一种弱类型或者说动态语言.这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定. 这也意味着你可以使用同一个变量保存不同类型的数据. 最新的 ECMAScrip ...

随机推荐

  1. 怎样做出优秀的扁平化设计风格 PPT 或 Keynote 幻灯片演示文稿?(装)

    不知道你有没有想过,为什么很人多的扁平化 PPT 是这个样子: 或者是这样: 然而,还有一小撮人的扁平化 PPT 却拥有那么高颜值: 为什么会产生这么大的差距呢?丑逼 PPT 应该如何逆袭成为帅逼呢? ...

  2. Eclipse插件 —— Maven的安装

    1.下载插件 下载一(CSDN 网站下载) CSDN上提供的下载内容是笔者在SOURCEFORGE网站上下载下来的.        由于SOURCEFORGE网站上有多个版本,且没有集中打包,需逐个下 ...

  3. cookie使用

    知识拷贝. 理论很简单,而且模式也和大多请求 http://blog.csdn.net/lanmao100/article/details/2328491(源地址).返回状态的SSO差不多.但是有几个 ...

  4. [转载] 推荐的C++书籍以及阅读顺序

    2014-06-17 转载自 oiramario 的文章 推荐的C++书籍以及阅读顺序 当读者有一定c/c++基础 推荐的阅读顺序: level 1 从<<essential c++> ...

  5. 浅析JavaScript引用类型之--Object、Array

    1.Object类型 对象是某个特定引用类型的实例,新对象有两种创建方式: i.使用new操作符调用构造函数来创建. var person = new Object(); person.name = ...

  6. 转:asmx迷10分钟升级成wcf熟手指南

    前言:本文旨在帮助从未接触过wcf(.svc文件)的webservice开发人员,快速将传统的webService/asmx技术迁移到wcf.高手就不用浪费时间往下看了:) 以下所有操作均为vs201 ...

  7. LA 3516 Exploring Pyramids (记忆化搜索)

    题意 给定一个DFS序列,问能有多少树与之对应. 思路 设输入序列为S,dp(i, j)为子序列Si, Si+1, --, Sj对应的树的个数,则边界条件为d(i, i) = 1,且Si != Sj时 ...

  8. 添加第三方类库造成的linker command failed with exit code 1 (use -v to see invocation)的错误调试

    linker command failed with exit code 1 (use -v to see invocation)这个错误貌似遇见并不止一次,当我想用某个第三方类库的时候(如SBJso ...

  9. HDU 5273 Dylans loves sequence (逆序对,暴力)

    题意: 给定一个序列,对于q个询问:(L,R)之间有几个逆序对?序列元素个数上限1000,q上限10万.仅1测试例子. 思路: [L,R]的逆序对数量可以这么算,假设L<=K<R,将区间拆 ...

  10. *ecshop 模板中foreach用法详解

    1.foreach分以下几个参数 from, item, name, iteration, index 2.使用foreach循环      如果php要传递一个数组(如:$array)给ecshop ...