【翻译】JavaScript中{}+{}是多少?

赵一楠 发表在:编程 分类下 2018年4月10日

原文:What is {} + {} in JavaScript?

title image

Gary Bernhardt最近在他的“Wat”演讲中,指出了一个JavaScript语言有趣而又奇怪之处:当你把对象和数组相加时,会得到意想不到的结果。 本文将解释逐一进行解释。

JavaScript的加法规则其实很简单:你只能使用数字和字符串进行相加,而所有其他数据类型都将被转换为其中一种。为了理解类型转换的原理,我们首先需要了解一些基础知识。

注意:本文中提到的章节(比如第9.1节)都出自ECMA-262语言规范(ECMAScript 5.1)。

让我们先快速的复习一下:JavaScript中有两种类型的值:原始值(primitive)和对象(object)[1]。 原始值包括:undefinednullbooleannumberstring。其他所有类型的值都是对象,如:arrayfunction等。

1.类型转换

加法运算符会触发三种类型转换:它将值转换为原始值、numberstring

1.1. 用 ToPrimitive() 将值转换为原始值

ToPrimitive()的使用语法如下:

ToPrimitive(input, PreferredType?)

可选参数PreferredType可以是NumberString类型。它仅表示期望的转换类型,而最终结果可以是任何原始类型值。假如PreferredTypeNumber,则将通过以下步骤完成input值的类型转换(第9.1节):

  1. input是原始类型值,则按原样返回。
  2. 如并非原始类型值而是对象,则调用obj.valueOf()。如结果为原始类型值,则直接返回该值。
  3. 如返回的并非原始类型值,则调用obj.toString()。如结果为原始类型值,则直接返回该值。
  4. 如返回的并非原始类型值,则抛出TypeError错误。

PreferredTypeString,则将步骤2和步骤3对调。如并未给出PreferredType,对于Date类型实例该值默认为String,而对于所有其他值该值默认为Number

1.2. 用 ToNumber() 将值转换为数字

下表解释了ToNumber()如何将原始类型值转换为数字的(第9.3节):

参数 结果
undefined NaN
null +0
boolean true转换为1,false转换为+0
number 不用转换
string 转换字符串中的数字,例如:将"324"转换为324

通过调用对象objToPrimitive(obj,Number)方法,对于得到的(原始类型的)结果调用ToNumber()将其转换为数字。

1.3. 用 ToString() 将值转换为字符串

下表解释了ToString()如何将原始类型值转换为字符串(第9.8节):

参数 结果
undefined undefined
null null
boolean "true""false"
number 原数字的字符串书写方式,比如:"1.765"
string 不用转换

通过调用对象objToPrimitive(obj,String)方法,对于得到的(原始类型的)结果调用ToString()将其转换为字符串。

1.4. 实践

通过以下对象,你将看到引擎内部的转换过程:

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

2.加法

对于如下加法运算:

value1 + value2

执行该表达式时,内部运算逻辑如下(第11.6.1节):

  1. 将两个参加运算的值转换为原始类型值(以下是数学表示法,而非JavaScript代码):
    prim1 := ToPrimitive(value1)
    prim2 := ToPrimitive(value2)
    
    此处省略了PreferredType,因此对于Date类型的值该值默认为String,其他类型的值该值默认为Number
  2. 如果prim1或prim2有一个是字符串,则将另一个也转换为字符串,最终返回字符串拼接后的结果。
  3. 如果都不是字符串,则将prim1和prim2都转换为数字,并返回他们之和。

2.1. 符合预期结果

当您将两个数组相加时,结果符合我们的预期:

  > [] + []
  ''

将[]转换为原始类型值,首先会调用valueOf()方法,最终返回数组本身(this):

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

由于结果并非原始类型值,接下来将调用toString()方法,返回空字符串(原始类型值)。这样一来,[]+[]的结果,其实就是两个空字符串拼接的值。

将数组和对象相加也符合我们的预期:

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

说明:将空对象转换为字符串的结果如下:

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

因此,上一个表达式的结果就应该是"""[object Object]"的字符串拼接的值。

更多将对象转换为原始类型值的例子:

  > 5 + new Number(7)
  12
  > 6 + { valueOf: function () { return 2 } }
  8
  > "abc" + { toString: function () { return "def" } }
  'abcdef'

2.2. 非预期结果

如果+的头一个运算值是空对象字面量(在Firefox控制台输出的结果):

  > {} + {}
  NaN

这是怎么回事儿?这是由于JavaScript将第一个{}解析为空代码块并忽略了。因此,通过计算+ {}(加号和第二个{})最终得到NaN。 这里的加号并非二元加法运算符,而是一元运算符前缀,它将其运算值转换为数字,其方法与Number()相同,例如:

  > +"3.65"
  3.65

以下表达式的结果都相同:

  +{}
  Number({})
  Number({}.toString())  // {}.valueOf() 并非原始类型值
  Number("[object Object]")
  NaN

为什么第一个{}被解析为代码块? 原因是整个输入内容被解析成了一段语句,而以开头花括号的语句会被解析为代码块。 你也可以强制把输入内容解析为表达式,从而来修正计算结果:

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

另外,函数或方法的参数也会被解析为表达式:

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

经过前面的讲解,见到下面的计算结果,你应该不会感到惊讶:

  > {} + []
  0

同样,上述语句也被解析为代码块和+ []。以下表达式的结果都相同:

  +[]
  Number([])
  Number([].toString())  // [].valueOf() 并非原始类型值
  Number("")
  0

有趣的是,Node.js的REPL在解析类似的输入时,与Firefox和Chrome(同Node.js一样使用V8引擎)的解析结果不同。以下输入被解析为表达式,结果符合我们预期:

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

它好处在于,其结果更像是将输入作为console.log()参数时所得到的结果。而非将输入用在程序语句中所得到的结果。

3.总结

大多数情况下,理解JavaScript中+的工作原理并不难:您只能将数字或字符串相加。 对象将被转换为字符串(如另一运算值是字符串的话)或数字(如另一运算值并非字符串)。 如需合并数组,则需要使用以下方法:

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

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中的假运算符重载”。

4.索引

  1. JavaScript中的值:并非所有都是对象
user avatar
取消

0 条评论