An old dog learns code

加法操作符

September 18, 2018


引子

JavaScript上手快 1 年了,今天无意刷到了几道传统面试题,无法有条理的给出答案,顿感基础薄弱。 这几道题都是关于加法操作符的知识点,先上面试例题:

1. []+[] //''

2. []+{}//'[object Object]'

3. {}+[]//0

4. {}+{}//'[object Object][object Object]'

下面先总结加法操作符规则,之后按照总结的计算规则,依次解题。

+操作符规则

"+"作为二元操作符,完成表达式x+y中 2 个变量数值相加或字符串拼接,具体规则如下所示:

  1. 如果 x, y 至少有一个为对象,将其转换为基本类型
  2. 转换后如果 x, y 至少有一个为string,则将另一个转换为string,执行字符串拼接
  3. 以上均不满足,将 x, y 转换为number,执行数值计算
对象转换为基本类型规则
  1. 如果 x 是Date对象,返回x.toString()
  2. 如果 x 是其他对象,且x.valueOf()返回值为基本类型,返回x.valueOf()
  3. 如果x.valueOf()返回值不是基本类型,且 x.toString()返回值为基本类型,返回x.toString()
  4. 以上皆不满足,抛出TypeError异常

    注:根据 ECMA,对象到基本类型转换实际过程较复杂,以后单独写一篇

题目解析

题 1

[] + []; //''

分析:

  • 均为对象,先将其转换为基本类型,根据对象转换规则,数组首先调用 valueOf(),返回值为自身,继续调用 toString(),返回空字符串'',表达式变为''+''
  • 根据+规则-2,执行字符串拼接,表达式返回结果''

题 2

[] + {}; //'[object Object]'

分析:

  • 均为对象,先将其转换为基本类型,根据对象转换规则,首先调用valueOf(),返回值为自身,继续调用toString(),返回空字符串'''[object Object]',表达式变为''+'[object Object]'
  • 根据+规则-2,执行字符串拼接,表达式返回结果'[object Object]'

题 3

{
}
+[]; //0

分析:

  • 因浏览器将{}解析为代码块注[1],表达式等价于+[],先将其转换为基本类型,根据对象转换规则,数组首先调用valueOf(),返回值为自身,继续调用toString(),返回空字符串'',表达式变为+'',取Number(''),表达式返回0
  • 如果将表达式改写为({})+[],将得到与题 2 相同的结果

题 4

{
}
+{}; //'[object Object][object Object]'

分析:

  • 这道题比较奇怪
  • 按照题 3 的规则,第一个{}将被解析为代码块,表达式等效于+{},按照对象转换规则,先变成+'[object Object]',然后取Number('[object Object]'),应该返回结果NaN才对。FireFox 62EdgeIE11的输出就是这个。
  • 可是输入Chrome 69,二者都是被当作对象处理的,最终表达式返回结果'[object Object][object Object]'
  • 看来几家浏览器在这种情况下规则有所不同,深入研究需要看parser里的抽象语法树Abstract Syntax Tree(AST) 是如何解析的,以后有时间了再研究

下面对题 4 做一点扩展,算作验证
题 4-扩展

1. ({})+{}   //Chrome  - '[object Object][object Object]'
             //FireFox - '[object Object][object Object]'
             //IE 11   - '[object Object][object Object]'
             //Edge    - '[object Object][object Object]'

2. {}+({})   //Chrome  - NaN
             //FireFox - NaN
             //IE 11   - NaN
             //Edge    - NaN

3. {}+{}+{}  //Chrome  - '[object Object][object Object][object Object]'
             //FireFox - 'NaN[object Object]'
             //IE 11   - 'NaN[object Object]'
             //Edge    - 'NaN[object Object]'

4. ({})+{}+{}//Chrome  - '[object Object][object Object][object Object]'
             //FireFox - '[object Object][object Object][object Object]'
             //IE 11   - '[object Object][object Object][object Object]'
             //Edge    - '[object Object][object Object][object Object]'

5. {}+({})+{}//Chrome  - '[object Object][object Object][object Object]'
             //FireFox - 'NaN[object Object]'
             //IE 11   - 'NaN[object Object]'
             //Edge    - 'NaN[object Object]'

6. {}+{}+({})//Chrome  - 'NaN[object Object]'
             //FireFox - 'NaN[object Object]'
             //IE 11   - 'NaN[object Object]'
             //Edge    - 'NaN[object Object]'

7. {}+[]+{}  //Chrome  - '[object Object][object Object]'
             //FireFox - '0[object Object]'
             //IE 11   - '0[object Object]'
             //Edge    - '0[object Object]'

8. {}+{}+[]  //Chrome  - 'NaN'
             //FireFox - 'NaN'
             //IE 11   - 'NaN'
             //Edge    - 'NaN'

看起来似乎Chrome对以{}开头且以{}结尾(即'{}+...+{}') 的表达式做了优化解析,不会把遇见的{}视为代码块,而是作为空对象解析。其他浏览器没有这个规则。


注 1

Expression Statement Syntax

ExpressionStatement[Yield] :
       [lookahead ∉ {{, function, class, let [}] Expression[In, ?Yield] ;

NOTE An ExpressionStatement cannot start with a U+007B (LEFT CURLY BRACKET) because that might make it ambiguous with a Block. Also, an ExpressionStatement cannot start with the function or class keywords because that would make it ambiguous with a FunctionDeclaration, a GeneratorDeclaration, or a ClassDeclaration. An ExpressionStatement cannot start with the two token sequence let [ because that would make it ambiguous with a let LexicalDeclaration whose first LexicalBinding was an ArrayBindingPattern.