上面的代码,跟之前单行 var a = 12; 是等价的。这个特性可以让开发者通过增加代码的可读性,更灵活地组织语言风格。平时写的跨多行数组、字符串拼接、链式调用等都属于这一类。
当然了,在省略分号的风格中,这种特性会导致一些意外的情况。举个例子:
var a
, b = 12
, hi = 2
, g = {exec: function() { return 3 }}
a = b
/hi/g.exec('hi')
console.log(a)
// 打印出 2, 因为代码会被解析成:
// a = b / hi / g.exec('hi');
// a = 12 / 2 / 3
以上例子,以 / 开头的正则表达式被解析器理解成除法运算,所以打印结果是 2。
其实,这不是省略分号风格的错误,而是开发者没有理解 JavaScript 解析器的工作原理。如果你偏向于省略分号的,那更要理解 ASI 的原理了。
解析器一个一个读取 token,但读到第二个 token b 时,它就发现没法构成合法的语句,然后它发现 token b 和上一个 token a 是有换行的,于是按照 规则 1.1 的处理,在 token b 之前插入分号变成 a\n;b,这样语法就合法了。接着继续读取 token,这时读到文件末尾,token b 还是不能构成合法的语句,这是按照 规则 2 处理,在末尾插入分号终止。故得到:
a
;b;
例二:大括号
{ a } b
解析器仍然一个一个读取 token,读到 token } 时,它发现 { a } 是不合法的,因为 a 是表达式,它必须以分号结尾。但是当前 token 是 },所以按照 规则 1.2 处理,他在 } 前面插入分号变成 { a; },接着往后读取 token b,按照 规则 2 给 b 加上分号。故得到:
前言
关于要不要加分号的问题,其实有很多争论!有的坚持加分号,而有的不喜欢加分号...但是无论那种风格,都不能百分百避免某些特殊情况产生的问题,究其根本就是因为对 JavaScript 解析和 ASI 规则的不了解。
对于此问题,看看几位大佬是怎么说的:
ASI 是什么?
按照 ECMAScript 标准,一些特定语句(statement)必须以分号结尾。分号代表这段语句的终止。但是有时候为了方便,这些分号是有可以省略的。这种情况下解析器会自己判断语句该在哪里终止。这种行为被叫做「自动插入分号」,简称 ASI(Automatic Semicolon Insertion)。
这些特定的语句有:
空语句
let
const
import
export
变量赋值
表达式
dubugger
continue
break
return
throw
ASI 会按照一定的规则去判断,应该在哪里插入分号。但在此之前,先了解一下 JavaScript 是如何解析代码的。
Token
解析器在解析代码时,会把代码分成很多
token
,一个token
相当于一小段有特定意义的语法片段。看例子:
通过 Esprima Parser 可以看到被拆分为多个
token
。以上变量声明语句可以分成五个
token
:var
关键字a
标识符=
标点符号12
数字;
标点符号解析器在解析语句时,会一个一个地读入
token
尝试构成给一个完整的语句(statement),直到碰到特定情况(例如语法规定的终止)才会认为这个语句结束了。这个例子中的终止符就是分号。用token
构成语句的过程类似于正则里的贪婪匹配,解释器总是试图用尽可能多的token
构成语句。敲重点!!!
任意
token
之间都可以插入一个或多个换行符(Line Terminator),这完全不会影响 JavaScript 的解析。上面的代码,跟之前单行
var a = 12;
是等价的。这个特性可以让开发者通过增加代码的可读性,更灵活地组织语言风格。平时写的跨多行数组、字符串拼接、链式调用等都属于这一类。当然了,在省略分号的风格中,这种特性会导致一些意外的情况。举个例子:
以上例子,以
/
开头的正则表达式被解析器理解成除法运算,所以打印结果是2
。其实,这不是省略分号风格的错误,而是开发者没有理解 JavaScript 解析器的工作原理。如果你偏向于省略分号的,那更要理解 ASI 的原理了。
ASI 规则
ECMAScript 标准定义的 ASI 包括三条规定和两条例外。
三条规则
token
),当碰到一个不能构成合法语句的token
时,他会在以下几种情况中,在该token
之前插入分号,此时不合群的token
被称为违规 token
(offending token
)token
跟上一个token
之间有至少一个换行。token
是}
。token
是)
它会试图把签名的token
理解成do-while
语句并插入分号。restricted production
的语法(比如return
),并且在restricted production
规定的[no LineTerminator here]
的地方发现换行,那么换行的地方就会被插入分号。两个例外
for
语句的两个分号之一。到这里,好像规则挺晦涩的。先别慌,看完下面的例子就能明白其中的含义了。
举例说明
就以上规则,举例说明助于理解。
例一:换行
解析器一个一个读取
token
,但读到第二个token b
时,它就发现没法构成合法的语句,然后它发现token b
和上一个token a
是有换行的,于是按照规则 1.1
的处理,在token b
之前插入分号变成a\n;b
,这样语法就合法了。接着继续读取token
,这时读到文件末尾,token b
还是不能构成合法的语句,这是按照规则 2
处理,在末尾插入分号终止。故得到:例二:大括号
解析器仍然一个一个读取
token
,读到token }
时,它发现{ a }
是不合法的,因为a
是表达式,它必须以分号结尾。但是当前token
是}
,所以按照规则 1.2
处理,他在}
前面插入分号变成{ a; }
,接着往后读取token b
,按照规则 2
给b
加上分号。故得到:例三:do-while 语句
这个是为了解释
规则 1.3
,这是最绕的地方。这例子种解析到
token c
的时候就不对了。这里既没有换行
,也没有}
,但token c
前面是token )
,所以解析器把之前的token
组成一个语句,并判断是否为do-while
语句,结果正好是的,于是自动插入分号变成do a; whiile(b);
,这种给token c
加上分号。故得到:简单来说,
do-while
后面的分号是自动插入的。但是如果其他以)
结尾的情况就不行了。规则 1.3
就是为do-while
量身定做的。例四:return
我们都知道
return
和返回值
之间不能换行,因为上面代码会解析成:但为什么不能换行呢?是因为
return
语句就是一个restricted production
语法。restricted production
是一组有严格限定的语法的统称,这些语法都是在某个地方不能换行的,不能换行的地方会被标注[no LineTermiator here]
。比如 ECMAScript 的
return
语法定义如下:这表示
return
跟表达式之间是不允许换行的(但后面的表达式内部可以换行)。如果这个地方恰好有换行,ASI 就会自动插入分号,这就是规则 3
的含义。刚才我们说了
restricted production
是一组语法的统称,它一共包含下面几个语法:++
和--
return
continue
break
throw
yield
这些无需死记,因为按照一般的书写习惯,几乎没有人会这样换行的。顺带一提,
continue
和break
后面是可以接 label 的。但这不在本文讨论范围内,有兴趣自行探索。例五:后缀表达式
解析器读到
token ++
时发现语句不合法(注意:++
不是两个token
,而是一个)。因为后缀表达式是不允许换行的。换句话说,换行的都不是后缀表达式,所以它只能按照规则 1.1
在token ++
前面插入分号来结束语句a
,然后继续执行,因为前缀表达式并不是restricted production
,所以++
和b
可以组成一条语句,然后按照规则 2
在末尾加上分号。故得到:例六:空语句
解析器解析到
token else
时发现不合法(else
是不能跟在if
语句头后面的),本来按照规则 1.1
,它应该加上分号变成if (a)\n;
,但是这样;
就变成空语句了,所以按照例外 1
,这个分号不能加,程序在else
处抛异常结束。Node.js 的运行结果:而以下这样语法是正确的。
例七:for 语句
解析器读到
token )
时发现语法不合法,本来换行可以自动插入分号,但是按照例外 2
,不能为for
头部自动插入分号,于是程序在)
处抛异常结束。Node.js 运行结果如下:如何手动测试 ASI
我们很难有办法去测试 ASI 是不是如预期那样工作的,只能看到代码最终执行结果是对是错。ASI 也没有手动打开或者关掉去对比结果。但我们可以通过对比解析器生成的 tree 是否一致来判断 ASI 插入的分号是不是跟我们预期的一致。这点可以用 Esprima Parser 验证。
举个例子:
Esprima 解析的 Syntax 如下所示(不需要看懂,记住大概样子就好):
然后我们主动加上分号,输入进去:
你就会发现
do a; while(b) c
和do a; while(b); c;
生成的 Syntax 是一致的。这说明解析器对这两段代码解析过程是一致的,我们并没有加入任何多余的分号。然后试试这个多余分号的版本:
它的结果是:
你会发现多出来一条空语句(EmptyStatement),那么这个分号就是多余的。
最后
看到这里,相信你对 JavaScript 解析机制和 ASI 机制有一个大致的了解了。
比如下面例子,这不是加不加分号的问题,而是懂不懂 JavaScript 解析的问题。
正如文章开头所说,我更偏向于不加分号的。这时候,我们要注意一些特殊情况,以避免 ASI 机制产生与我们编写程序的预期结果不一致的问题。
敲重点!!!
以
(
开头的情况JavaScript 解析器会解析成这样:
以
[
开头的情况JavaScript 解析器会按以下这样去解析,由于
function() {}[1,2,3]
返回值是undefined
,所以就会报错。以
/
开头的情况JavaScript 解析器会按以下这样去解析,所以就会报错。
以
+
开头的情况JavaScript 解析器会解析成这样:
以
-
开头的情况JavaScript 解析器会解析成这样:
所以理解了以上规则之后,我可以愉快了使用 ESLint + Prettier 一键去掉分号以及统一格式化,而不会有任何的负担了。
References