({}+10)vs {}+10 究竟是谁不讲武德? - 七日打卡

1,621 阅读7分钟

不知道年纪轻轻的你有没有过这种烦恼?

  • 遇到过的问题,换个场面(面试or同事问到)还是不会?
  • 一眼望去,简单简单,上手去做,直呼好难! 如果有,那么恭喜你小伙子,喜提“太自信”荣誉称号! 为了避免这些问题,我们就要放下身段,接受自己的帅气(美丽),好好记录、反思遇到过的问题才是发展之本

好了,扯了这么多,让我们接下来一起看看这道题你是否熟悉,或者说这次遇到的时候是不是还是知道答案,并且说出为什么呢?

抛转 - 问题

下面这两条语句,在浏览器的控制台里会打印出什么呢?


({}+10)

{}+10

这里就不和大家卖关子了,打印结果如下:

不知道上面的打印结果是否符合你的预期呢?

如果是,并且知道是为什么,那就不用在弟弟这篇文章中浪费时间啦,顺手点个赞就可以去get其他知识点了~

如果不是,那么我们就继续向下说道说道,首先我们先来把心里的疑惑屡清楚

梳理疑点

  1. 首先第一个加圆括号的语句打印出来的结果是符合我们的认知的

    • +运算符是有一个隐式转换的规则的,转换规则很复杂,针对我们这道题只需要知道,+运算符遇到一个对象和数字相加时会首先将对象调用toString方法进行转换,然后将数字转换成字符串,最后将两个字符串进行拼接。

    上面我们提到的转换,用行话说叫“拆箱”,针对Object类型的拆箱操作我们需要知道这些知识点

    1. Object进行类型转换的时候,会优先调用[symbol.toPrimitive]()
    2. Object遇到+号并且没有toPrimitive的时候会优先调用valueOf方法 (前提:需要转换成Number时就调用valueOf
    3. Object作为key属性存在时会优先调用toString方法(前提:需要转换成string时就调用toString
      • eg: Obj[key] 这里就会调用toString方法

    所以针对这个表达式,我们可以理解为以下操作:

      const obj = {}
      const str = obj.toString()
      console.log( str + 10 ) // "[object Object]10"
    
  2. 其次就是为什么第二个只是少了个圆括号,就会造成这样的差异呢?这应该是我们最困惑的点,其实这也是今天我想说的最重要的问题。

    对于这个问题,其实我想只要我换一种写法大家就会知道了: 看到了吗?加了分号之后是不是就比较符合我们的认知了,瞬间感觉第二个语句打印的结果是这么的顺其自然。

    所以这个分号是怎么回事呢?为什么我们第一次打印的明明没有加分号,打印出来的却和加了分号的结果一样呢?

    其实这里是因为js在解释执行的时候{}会被解析成BlockStatement(复合语句),相当于是一个整体,只不过这个块中没有任何内容而已,所以不会进行转换,只会对+10进行输出

    一切都清晰了,但是文章还没有因此结束,因为我们上面提到了加分号,虽然对于这个语句其实在js执行的时候是没有在gif图中的位置加分号的,我这里是为了让大家清晰的分辨前面是一个复合语句而做的处理。

    虽然它没加但是不证明它没有加分号的这个机制,其实在js里还真的有自动添加分号这么一个东西,并且影响这我们的代码

    那么就到了今天的“引玉”环节了。

引玉 - ASI

首先,我带大家先普及一下基本知识:

何为 ASI?

ASI(Automatic semicolon insertion: 中文全称“自动分号补全”。

ECMAScript标准中规定,一些特定的JavaScript语句必须要以分号结尾,用分号来标识这个语句的终点。但是我们有很多时候为了让代码简洁都是会省略分号的,这种行为在JavaScript中也是被允许的,因为在代码编译的时候解释器会帮助我们自动添加分号,那么对于解释器来说就有一套自己的规则去界定何时添加分号,我们这里简单总结一下:

特定的语句

所有特定语句可以看MDN官网:点我

  1. 空语句
  2. let、const、变量声明
  3. import、export、模块定义
  4. 表达式语句
  5. debugger
  6. continue、break、throw
  7. return

我们的代码在解析的时候会被从左到右扫描,并被转换为一系列的输入元素,包括 token、控制符、行终止符、注释和空白符。

也就是说对于上面的这些特定语句,解释器解析到的时候ASI机制就会主动识别并且添加上分号,下面我们具体来说一下这个过程:

  1. 遇到一个不被允许的行终止符符或者}

行终止符(line terminators):简单说来就是一些换行(\n)、回车之类(\r)的。大家可以从官网看的更详细的信息,这里就不赘述了

  { 1 2 } 3

  // ASI转换: 解析时遇到 } 在前面加上分号,继续解析发现3后面是个换行符再加上分号,就会变成下面这样子

  { 1 2 ;} 3;
  1. 当捕获到标识符输入流的结尾,并且无法将单个输入流转换为一个完整的程序时,将在结尾插入一个分号。 对于这点我的理解是,对于一个语句进行解析的时候,会对每个token进行解析,直到遇到类似终止符的情况结束。如果每个解析出来的token无法单独完成一个有意义的执行程序,那么这些token将会被组合在一起当做一个完整的执行语句(这个过程中解释器会将尽量多的token去当成一个完整的语句)。最后就会在末尾添加分号作为标识
  a = b
  ++c

解析器进行词法(token)分析的时候会进行类似这样的操作:

  a
  =
  b
  换行符
  +
  +
  c
  换行符

根据这个规则就会转换成下面的格式

  a = b;
  ++c;
  1. 当语句中包含一个[受限的语法并且后面跟着一个行终止符]的时候,将会在结尾插入一个分号。

受限的语法包括:

  • 后置运算符(++ 和 --)
  • continue
  • break
  • return
  • yield、yield*
  • module
 return
 a + b

// return 在受限语法之列,并且后面有换行符所以会直接在后面添加分号

return;
a + b;

所以这里我们需要注意: 在代码中 return 之后不要回车然后再带上需要返回的内容,因为这时return的就是undefined了

ECMA中对ASI的解释

链接在这里:ES2015

上面我们讲到的规则在这些定义里面都有体现,大家感兴趣的可以看一下官方的原文。

好了,今天就聊到这里了~因为这也是今天学习整理的,可能会有错误的地方,希望大家可以指出,也希望大家路过给码了2000字的弟弟点个赞