在 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 代码):

    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

天哪!神马情况?(译注:这个问题的原因是,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]'

在大多数情况下,想要弄明白 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}

JavaScript中{}+{}的更多相关文章

  1. javascript中的Array对象 —— 数组的合并、转换、迭代、排序、堆栈

    Array 是javascript中经常用到的数据类型.javascript 的数组其他语言中数组的最大的区别是其每个数组项都可以保存任何类型的数据.本文主要讨论javascript中数组的声明.转换 ...

  2. javascript中的this与函数讲解

    前言 javascript中没有块级作用域(es6以前),javascript中作用域分为函数作用域和全局作用域.并且,大家可以认为全局作用域其实就是Window函数的函数作用域,我们编写的js代码, ...

  3. JavaScript 中的数据类型

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

  4. javascript中的操作符详解1

    好久没有写点什么了,根据博主的技术,仍然写一点javascript新手入门文章,接下来我们一起来探讨javascript的操作符. 一.前言 javascript中有许多操作符,但是许多初学者并不理解 ...

  5. 掌握javascript中的最基础数据结构-----数组

    这是一篇<数据结构与算法javascript描述>的读书笔记.主要梳理了关于数组的知识.部分内容及源码来自原作. 书中第一章介绍了如何配置javascript运行环境:javascript ...

  6. javascript中变量提升的理解

    网上找了两个经典的例子 var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); // 10 var ...

  7. 前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型

    前端开发:面向对象与javascript中的面向对象实现(二)构造函数与原型 前言(题外话): 有人说拖延症是一个绝症,哎呀治不好了.先不说这是一个每个人都多多少少会有的,也不管它究竟对生活有多么大的 ...

  8. 简单分析JavaScript中的面向对象

    初学JavaScript的时候有人会认为JavaScript不是一门面向对象的语言,因为JS是没有类的概念的,但是这并不代表JavaScript没有对象的存在,而且JavaScript也提供了其它的方 ...

  9. Javascript中的valueOf与toString

    基本上,javascript中所有数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题,本文将详细介绍,有需要的朋友可以参考下. t ...

  10. 关于javascript中的this关键字

    this是非常强大的一个关键字,但是如果你不了解它,可能很难正确的使用它. 下面我解释一下如果在事件处理中使用this. 首先我们讨论一下下面这个函数中的this关联到什么. function doS ...

随机推荐

  1. JDBC学习1:详解JDBC使用

    什么是JDBC JDBC(Java Database Connectivity),即Java数据库连接,是一种用于执行SQL语句的Java API,可以为多种关系数据库提供同一访问,它由一组用Java ...

  2. var与this,{}与function 小记

    JavaScript var是用来定义一个变量,this常用来定义或调用一个属性或方法.但是在全局范围内,this和var定义变量是等价的. window console.log('window:', ...

  3. 一个简单的通用Makefile实现

    一个简单的通用Makefile实现   Makefile是Linux下程序开发的自动化编译工具,一个好的Makefile应该准确的识别编译目标与源文件的依赖关系,并且有着高效的编译效率,即每次重新ma ...

  4. [每日电路图] 8、三轴加速度计LIS3DH电路图及功耗等指标

    看TI的官网资料:http://www.st.com/web/en/catalog/sense_power/FM89/SC444/PF250725 一.初次接触关注的信息: 1.1.概述中的关键信息 ...

  5. java中基本类型和包装类型实践经验

    至今,小菜用java快两年了,有些事,也该有个总结. 基本类型和包装类型的概念在本文不作赘述. 如果这两种类型直接使用,倒没什么值得讨论的,无非就是自动装箱拆箱,java可以让你感觉不到他们的存在,但 ...

  6. 【译】用jQuery 处理XML-- DOM(文本对象模型)简介

    用jQuery 处理XML--写在前面的话 用jQuery 处理XML-- DOM(文本对象模型)简介 用jQuery 处理XML--浏览器中的XML与JavaScript 用jQuery 处理XML ...

  7. 杂谈X509证书, Java Keystore与Jetty

    很多人对JSSE组成部分的Key Tool 工具不太明白,希望本文能有帮助 科班出身的同学应该学过课程“密码学”, 这门课详细解释了现代对称加密的算法原理, 当时学的我云里雾里. 直到现在使用过SSL ...

  8. React Native02-开始运行 Android篇

    1. 开始运行 1)用命令进入到新建的文件目录下,比如HelloWorld,再输入 react-native start: 在等待一段时间后,我们看到最后面有个地址,说明已经运行成功了. 我们输入地址 ...

  9. gulp:更简单的自动化构建工具

    目前最流行的两种使用JavaScript开发的构建工具是Grunt和Gulp.为什么使用gulp?因为Gulp更简单.Grunt任务拥有大量的配置,会引用大量你实际上并不需要的对象属性,但是Gulp里 ...

  10. C#并行编程-线程同步原语

    菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...