JS的{} + {}与{} + []的结果是什么?

在JS中的运算符共同的状况中,(+)符号是很常见的一种,它有如下的使用状况:html

  • 数字的加法运算,二元运算数组

  • 字符串的链接运算,二元运算,最高优先浏览器

  • 正号,一元运算,可延伸为强制转换其余类型的运算元为数字类型函数

固然,若是考虑多个符号一块儿使用时,(+=)与(++)又是另外的用途。prototype

另外一个常见的是花括号({}),它有两个用途也很常见:设计

  • 对象的字面文字定义code

  • 区块语句htm

因此,要能回答这个问题,要先搞清楚重点是什么?对象

第一个重点是:教程

加号(+)运算在JS中在使用上的规定是什么。

第二个重点则是:

对象在JS中是怎么转换为原始数据类型的值的。

加号运算符(+)

除了上面说明的常见状况外,在标准中转换的规则还有如下几个,要注意它的顺序:

operand + operand = result

  1. 使用ToPrimitive运算转换左与右运算元为原始数据类型值(primitive)

  2. 在第1步转换后,若是有运算元出现原始数据类型是"字符串"类型值时,则另外一运算元做强制转换为字符串,而后做字符串的链接运算(concatenation)

  3. 在其余状况时,全部运算元都会转换为原始数据类型的"数字"类型值,而后做数学的相加运算(addition)

ToPrimitive内部运算

所以,加号运算符只能使用于原始数据类型,那么对于对象类型的值,要如何转换为原始数据类型?下面说明是如何转换为原始数据类型的。

ECMAScript 6th Edition #7.1.1,有一个抽象的ToPrimitive运算,它会用于对象转换为原始数据类型,这个运算不仅会用在加号运算符,也会用在关系比较或值相等比较的运算中。下面有关于ToPrimitive的说明语法:

ToPrimitive(input, PreferredType?)

input表明代入的值,而PreferredType能够是数字(Number)或字符串(String)其中一种,这会表明"优先的"、"首选的"的要进行转换到哪种原始类型,转换的步骤会依这里的值而有所不一样。但若是没有提供这个值也就是预设状况,则会设置转换的hint值为"default"。这个首选的转换原始类型的指示(hint值),是在做内部转换时由JS视状况自动加上的,通常状况就是预设值。

而在JS的Object原型的设计中,都必定会有两个valueOftoString方法,因此这两个方法在全部对象里面都会有,不过它们在转换有可能会交换被调用的顺序。

当PreferredType为数字(Number)时

PreferredType为数字(Number)时,input为要被转换的值,如下是转换这个input值的步骤:

  1. 若是input是原始数据类型,则直接返回input

  2. 不然,若是input是个对象时,则调用对象的valueOf()方法,若是能获得原始数据类型的值,则返回这个值。

  3. 不然,若是input是个对象时,调用对象的toString()方法,若是能获得原始数据类型的值,则返回这个值。

  4. 不然,抛出TypeError错误。

当PreferredType为字符串(String)时

上面的步骤2与3对调,如同下面所说:

  1. 若是input是原始数据类型,则直接返回input

  2. 不然,若是input是个对象时,调用对象的toString()方法,若是能获得原始数据类型的值,则返回这个值。

  3. 不然,若是input是个对象时,则调用对象的valueOf()方法,若是能获得原始数据类型的值,则返回这个值。

  4. 不然,抛出TypeError错误。

PreferredType没提供时,也就是hint为"default"时

PreferredType为数字(Number)时的步骤相同。

数字实际上是预设的首选类型,也就是说在通常状况下,加号运算中的对象要做转型时,都是先调用valueOf再调用toString

但这有两个异常,一个是Date对象,另外一是Symbol对象,它们覆盖了原来的PreferredType行为,Date对象的预设首选类型是字符串(String)。

所以你会看到在一些教程文件上会区分为两大类对象,一类是 Date 对象,另外一类叫 非Date(non-date) 对象。由于这两大类的对象在进行转换为原始数据类型时,首选类型刚好相反。

模拟代码说明

以简单的模拟代码来讲明,加号运算符(+)的运行过程就是像下面这个模拟码同样,我想这会很容易理解:

a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)

    if(pa is string || pb is string)
       return concat(ToString(pa), ToString(pb))
    else
       return add(ToNumber(pa), ToNumber(pb))

步骤简单来讲就是,运算元都用ToPrimitive先转换为原始数据类型,而后其一是字符串时,使用ToString强制转换另外一个运算元,而后做字符串链接运算。要否则,就是都使用ToNumber强制转换为数字做加法运算。

ToPrimitive在遇到对象类型时,预设调用方式是先调用valueOf再调用toString,通常状况数字类型是首选类型。

上面说的ToStringToNumber这两个也是JS内部的抽象运算。

valueOf与toString方法

valueOfToString是在Object中的两个必有的方法,位于Object.prototype上,它是对象要转为原始数据类型的两个姐妹方法。从上面的内容已经能够看到,ToPrimitive这个抽象的内部运算,会依照设置的首选的类型,决定要前后调用valueOftoString方法的顺序,当数字为首选类型时,优先使用valueOf,而后再调用toString。当字符串为首选类型时,则是相反的顺序。预设调用方式则是如数字首选类型同样,是先调用valueOf再调用toString

JS对于Object与Array的设计

在JS中所设计的Object纯对象类型的valueOftoString方法,它们的返回以下:

  • valueOf方法返回值: 对象自己。

  • toString方法返回值: "[object Object]"字符串值,不一样的内建对象的返回值是"[object type]"字符串,"type"指的是对象自己的类型识别,例如Math对象是返回"[object Math]"字符串。但有些内建对象由于覆盖了这个方法,因此直接调用时不是这种值。(注意: 这个返回字符串的前面的"object"开头英文是小写,后面开头英文是大写)

你有可能会看过,利用Object中的toString来进行各类不一样对象的判断语法,这在之前JS能用的函数库或方法很少的年代常常看到,不过它须要配合使用函数中的call方法,才能输出正确的对象类型值,例如:

> Object.prototype.toString.call([])
"[object Array]"

> Object.prototype.toString.call(new Date)
"[object Date]"

因此,从上面的内容就能够知道,下面的这段代码的结果会是调用到toString方法(由于valueOf方法的返回并非原始的数据类型):

> 1 + {}
"1[object Object]"

一元正号(+),具备让首选类型(也就是hint)设置为数字(Number)的功能,因此能够强制让对象转为数字类型,通常的对象会转为:

> +{} //至关于 +"[object Object]"
NaN

固然,对象的这两个方法均可以被覆盖,你能够用下面的代码来观察这两个方法的运行顺序,下面这个都是先调用valueOf的状况:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return 'obj'; // string
  }
}
console.log(1 + obj);  //valueOf -> toString -> '1obj'
console.log(+obj); //valueOf -> toString -> NaN
console.log('' + obj); //valueOf -> toString -> 'obj'

先调用toString的状况比较少见,大概只有Date对象或强制要转换为字符串时才会看到:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return 1; // number
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}
alert(obj); //toString -> valueOf -> alert("1");
String(obj); //toString -> valueOf -> "1";

而下面这个例子会形成错误,由于不论顺序是如何都得不到原始数据类型的值,错误消息是"TypeError: Cannot convert object to primitive value",从这个消息中很明白的告诉你,它这里面会须要转换对象到原始数据类型:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}

console.log(obj + obj);  //valueOf -> toString -> error!

Array(数组)很经常使用到,虽然它是个对象类型,但它与Object的设计不一样,它的toString有覆盖,说明一下数组的valueOftoString的两个方法的返回值:

  • valueOf方法返回值: 对象自己。(与Object同样)

  • toString方法返回值: 至关于用数组值调用join(',')所返回的字符串。也就是[1,2,3].toString()会是"1,2,3",这点要特别注意。

Function对象不多会用到,它的toString也有被覆盖,因此并非Object中的那个toString,Function对象的valueOftoString的两个方法的返回值:

  • valueOf方法返回值: 对象自己。(与Object同样)

  • toString方法返回值: 函数中包含的代码转为字符串值

Number、String、Boolean三个包装对象

包装对象是JS为原始数据类型数字、字符串、布尔专门设计的对象,全部的这三种原始数据类型所使用到的属性与方法,都是在这上面所提供。

包装对象的valueOftoString的两个方法在原型上有通过覆盖,因此它们的返回值与通常的Object的设计不一样:

  • valueOf方法返回值: 对应的原始数据类型值

  • toString方法返回值: 对应的原始数据类型值,转换为字符串类型时的字符串值

toString方法会比较特别,这三个包装对象里的toString的细部说明以下:

  • Number包装对象的toString方法: 能够有一个传参,能够决定转换为字符串时的进位(二、八、16)

  • String包装对象的toString方法: 与String包装对象中的valueOf相同返回结果

  • Boolean包装对象的toString方法: 返回"true"或"false"字符串

另外,常被搞混的是直接使用Number()String()Boolean()三个强制转换函数的用法,这与包装对象的用法不一样,包装对象是必须使用new关键字进行对象实例化的,例如new Number(123),而Number('123')则是强制转换其余类型为数字类型的函数。

Number()String()Boolean()三个强制转换函数,所对应的就是在ECMAScript标准中的ToNumberToStringToBoolean三个内部运算转换的对照表。而当它们要转换对象类型前,会先用上面说的ToPrimitive先转换对象为原始数据类型,再进行转换到所要的类型值。

无论如何,包装对象不多会被使用到,通常咱们只会直接使用原始数据类型的值。而强制转换函数由于也有替换的语法,它们会被用到的机会也很少。

实例

字符串 + 其余原始类型

字符串在加号运算有最高的优先运算,与字符串相加一定是字符串链接运算(concatenation)。全部的其余原始数据类型转为字符串,能够参考ECMAScript标准中的ToString对照表,如下为一些简单的例子:

> '1' + 123
"1123"

> '1' + false
"1false"

> '1' + null
"1null"

> '1' + undefined
"1undefined"

数字 + 其余的非字符串的原始数据类型

数字与其余类型做相加时,除了字符串会优先使用字符串链接运算(concatenation)的,其余都要依照数字为优先,因此除了字符串以外的其余原始数据类型,都要转换为数字来进行数学的相加运算。若是明白这项规则,就会很容易的得出加法运算的结果。

全部转为数字类型能够参考ECMAScript标准中的ToNumber对照表,如下为一些简单的例子:

> 1 + true //true转为1, false转为0
2

> 1 + null //null转为0
1

> 1 + undefined //undefined转为NaN
NaN

数字/字符串之外的原始数据类型做加法运算

当数字与字符串之外的,其余原始数据类型直接使用加号运算时,就是转为数字再运算,这与字符串彻底无关。

> true + true
2

> true + null
1

> undefined + null
NaN

空数组 + 空数组

> [] + []
""

两个数组相加,依然按照valueOf -> toString的顺序,但由于valueOf是数组自己,因此会以toString的返回值才是原始数据类型,也就是空字符串,因此这个运算至关于两个空字符串在相加,依照加法运算规则第2步骤,是字符串链接运算(concatenation),两个空字符串链接最后得出一个空字符串。

空对象 + 空对象

> {} + {}
"[object Object][object Object]"

两个空对象相加,依然按照valueOf -> toString的顺序,但由于valueOf是对象自己,因此会以toString的返回值才是原始数据类型,也就是"[object Object]"字符串,因此这个运算至关于两个"[object Object]"字符串在相加,依照加法运算规则第2步骤,是字符串链接运算(concatenation),最后得出一个"object Object"字符串。

可是这个结果有异常,上面的结果只是在Chrome浏览器上的结果(v55),怎么说呢?

有些浏览器例如Firefox、Edge浏览器会把{} + {}直译为至关于+{}语句,由于它们会认为以花括号开头({)的,是一个区块语句的开头,而不是一个对象字面量,因此会认为略过第一个{},把整个语句认为是个+{}的语句,也就是至关于强制求出数字值的Number({})函数调用运算,至关于Number("[object Object]")运算,最后得出的是NaN

特别注意: {} + {}在不一样的浏览器有不一样结果

若是在第一个(前面)的空对象加上圆括号(()),这样JS就会认为前面是个对象,就能够得出一样的结果:

> ({}) + {}
"[object Object][object Object]"

或是分开来先声明对象的变量值,也能够得出一样的结果,像下面这样:

> let foo = {}, bar = {};
> foo + bar;

注: 上面说的行为这与加号运算的第一个(前面)的对象字面值是否是个空对象无关,就算是里面有值的对象字面,例如{a:1, b:2},也是一样的结果。

注: 上面说的Chrome浏览器是在v55版本中的主控台直接运行的结果。其它旧版本有可能并不是此结果。

空对象 + 空数组

上面一样的把{}看成区块语句的状况又会发生,不过此次全部的浏览器都会有一致结果,若是{}(空对象)在前面,而[](空数组)在后面时,前面(左边)那个运算元会被认为是区块语句而不是对象字面量。

因此{} + []至关于+[]语句,也就是至关于强制求出数字值的Number([])运算,至关于Number("")运算,最后得出的是0数字。

> {} + []
0

> [] + {}
"[object Object]"

特别注意: 因此若是第一个(前面)是{}时,后面加上其余的像数组、数字或字符串,这时候加号运算会直接变为一元正号运算,也就是强制转为数字的运算。这是个陷阱要当心。

Date对象

Date对象的valueOftoString的两个方法的返回值:

  • valueOf方法返回值: 给定的时间转为UNIX时间(自1 January 1970 00:00:00 UTC起算),可是以微秒计算的数字值

  • toString方法返回值: 本地化的时间字符串

Date对象上面有说起是首选类型为"字符串"的一种异常的对象,这与其余的对象的行为不一样(通常对象会先调用valueOf再调用toString),在进行加号运算时时,它会优先使用toString来进行转换,最后一定是字符串链接运算(concatenation),例如如下的结果:

> 1 + (new Date())
> "1Sun Nov 27 2016 01:09:03 GMT+0800 (CST)"

要得出Date对象中的valueOf返回值,须要使用一元加号(+),来强制转换它为数字类型,例如如下的代码:

> +new Date()
1480180751492

Symbols类型

ES6中新加入的Symbols数据类型,它不算是通常的值也不是对象,它并无内部自动转型的设计,因此彻底不能直接用于加法运算,使用时会报错。

总结

{} + {}的结果是会因浏览器而有不一样结果,Chrome(v55)中是[object Object][object Object]字符串链接,但其它的浏览器则是认为至关于+{}运算,得出NaN数字类型。

{} + []的结果是至关于+[],结果是0数字类型。

参考文章