自动分号插入机制( Automatic Semicolon Insertion,ASI)
按照 ECMAScript 标准,一些 特定语句需要用分号来表示语句的结束。但是有时候为了方便,这些分号可以在源码中省略的。这种情况下解析器会自己判断语句该在哪里终止(解析器除了分号还会以换行为基础按一定的规则作为断句的依据,从而保证解析的正确性)。这种行为被叫做 “自动插入分号”,简称 ASI (Automatic Semicolon Insertion) 。实际上分号并没有真的被插入,这只是个便于解释的形象说法。 这些特定的语句有:
空语句
let
、var
、const
、变量声明
、变量赋值
import
、export
、模块定义
表达式语句
debugger
continue
、break
、throw
return
上面的定义表示:
- 所有这些语句中的分号都是可以省略的。
- 除此之外其他的语句有两种特殊的情况,一是不需要分号的(比如for ,while(not do-while), switch, try,
if
和函数声明,注意:如果你在上面语句的末尾增加一个分号,不会有任何错误。 因为解释器会将它认为是一个空语句),二是分号不能省略的(比如for语句头部的分号
)。
那么 ASI 如何知道在哪里插入分号呢?它会按照一些规则去判断。但在说规则之前,我们先了解一下 JS 是如何解析代码的。
var result = 99 * 99
这行代码会被解析为以下形式
[
{
type: 'keyword', //关键字
value: 'var'
},
{
type: 'Identifier', //标识符
value: 'reslut'
},
{
type: 'Punctuator', //运算符
value: '='
},
{
type: 'Numberic', //数字
value: '99'
},
{
type: 'Punctuator', //运算符
value: '*'
},
{
type: 'Numberic', //数字
value: '99'
},
{
type: 'Punctuator', //运算符
value: ';'
},
]
解析器在解析代码时,会把代码分成很多 token 。一个 token 相当于一小段有特定意义的语法片段。 解释器在解析语句时会一个一个读入 token 尝试构成一个完整的语句 (statement),直到碰到特定情况(比如语法规定的终止)才会认为这个语句结束了。用 token 构成语句的过程类似于正则里的贪婪匹配,解释器总是试图用尽可能多的 token 构成语句,在实在不符合正确语法的情况下,才会退而求其次,启用 ASI 机制,将行结束符 (Line Terminator)分隔的token当作两条语句(俗称: 插入分号)。
接下来是重点:任意 token 之间都可以插入一个或多个行结束符,这完全不影响 JS 的解析,所以上面的代码可以写成下面这样(功能等价,不过在省略分号的风格中,这种解析特性会导致一些意外情况,行结束符之后的符号 (token) 有二义性,使得该符号与上条语句能够无缝对接,不导致语法错误):
let
result
=
99
*
99
;
新行以 ( [ / + - . , * % 等符号开始时,容易产生二义性
ASI规则
ECMAScript 标准定义的 ASI 包括 三条规则 和 两条例外。
三条规则是描述何时该自动插入分号:
-
解析器从左往右解析代码(读入 token),当碰到一个不能构成合法语句的 token 时,它会在以下几种情况中在该 token 之前插入分号,此时这个不合群的 token 被称为 offending token :
- 如果这个 token 跟上一个 token 之间有至少一个换行。 - 如果这个 token 是 `}`。 - 如果前一个 token 是 `)`,它会试图把前面的 token 理解成 `do...while` 语句并插入分号。
-
当解析到文件末尾发现语法还是有问题,就会在文件末尾插入分号。
-
当解析时碰到 restricted production 的语法(比如
return
),并且在 restricted production 规定的[no LineTerminator here]
的地方发现换行,那么换行的地方就会被插入分号。
两条例外表示,就算符合上述规则,如果分号会被解析成下面的样子,它也不能被自动插入:
1. 分号不能被解析成空语句。
2. 分号不能被解析成 `for` 语句头部的两个分号之一。
例子解析
第一个例子:换行
a
b
我们模拟一下解析器的思考过程,大概是这样的:解析器一个个读取 token ,但读到第二个 token b
时它就发现没法构成合法的语句,然后它发现 b
和前面是有换行的,于是按照规则一(情况一),它在 b
之前插入分号变成 a\n;b
,这样语句就合法了。然后继续处理,这时读到文件末了,b
还是不能构成合法的语句,这时候按照规则二,它在末尾插入分号,结束。最终结果是:
a
;b;
第二个例子:大括号
{ a } b
解析器仍然一个个读取 token ,读到 token "}"
时发现 { a }
是不合法的,因为 a
是表达式,它必须以分号结尾。但当前 token 是 }
,所以按照规则一(情况二),它在 }
前面插入分号变成 { a ;}
,这句就通过了,然后继续处理,按照规则二给 b
加上分号,结束。最终结果是:
{ a; } b;
顺带一提,也许有人会觉得 { a; };
这样才更自然。但 {...}
属于块语句,而按照定义块语句是不需要分号结尾的,不管是不是在一行。因为块语句也被用在其他地方(比如函数定义),所以下面这种代码也是完全合法的,不需要任何分号:
function a() {} function b () {}
第三个例子:do while
这个是为了解释规则一(情况三),这是最绕的部分,代码如下:
let a = 'cris'
let item = 0
do {
text += item
item++
} while( item < 5) a
这个例子中解析到 token c
的时候就不对了。这里面既没有换行也没有 }
,但 c
前面是 )
,所以解析器把之前的 token 组成一个语句,并判断该语句是不是 do...while
,结果正好是的!于是插入分号变成 do awhile(b);
,最后给 c
加上分号,结束。最终结果为:
let a = 'cris'
let item = 0
do {
text += item
item++
} while( item < 5);
a;
简单点说,do...while
后面的分号是会自动插入的。但如果其他以 )
结尾的情况就不行了, 会报错。规则一(情况三)就是为 do...while
量身定做的。
第四个例子:return
return
a
你一定知道 return
和返回值之间不能换行,因为上面代码会解析成:
a;
但为什么不能换行?因为 return
语句就是一个 restricted production。这是什么意思?它是一组有严格限定的语法的统称,这些语法都是在某个地方不能换行的,不能换行的地方会被标注 [no LineTerminator here]
。
比如 ECMAScript 的 return
语法定义如下:
return [no LineTerminator here] Expression;
这表示 return
跟表达式之间是不允许换行的(但后面的表达式内部可以换行)。如果这个地方恰好有换行,ASI 就会自动插入分号,这就是规则三的含义。
刚才我们说了 restricted production 是一组语法的统称,它一共包含下面几个语法:
LeftHandSideExpression [no LineTerminator here] ++
LeftHandSideExpression [no LineTerminator here] --
continue [no LineTerminator here] Identifier;
break [no LineTerminator here] Identifier;
return [no LineTerminator here] Expression;
throw [no LineTerminator here] Expression;
ArrowParameters [no LineTerminator here] => ConciseBody
这些不用死记,因为按照常规书写习惯,几乎没人会这样换行的。
第五个例子:后缀表达式
a
++
b
解析器读到 token ++
时发现语句不合法,因为后缀表达式是不允许换行的,换句话说,换行的都不是后缀表达式。所以它只能按照规则一(情况一)在 ++
前面加上分号来结束语句 a
,然后继续执行,因为前缀表达式并不是 restricted production ,所以 ++
和 b
可以组成一条语句,然后按照规则二在末尾加上分号。最终结果为:
第六个例子:空语句
if(a)
else b
解释器解析到 token else
时发现不合法,本来按照规则一(情况一),它在应该加上分号变成 if (a)\n;
,但这样 ;
就变成空语句了,所以按照例外一,这个分号不能加。程序在 else
处抛异常结束。Node.js 的运行结果:
else b
^^^^
SyntaxError: Unexpected token else
第七个例子:for
for(a;b
)
解析器读到 token )
时发现不合法,本来换行可以自动插入分号,但按照例外二,不能为 for
头部自动插入分号,于是程序在 )
处抛异常结束。Node.js 运行结果如下:
)
^
SyntaxError: Unexpected )
限制产生式
前文说到,ASI 是一种备用选择。然而在 ECMAScript 中,有几种特殊语句是不允许行结束符存在的。如果语句中有行结束符,解析器会优先认为行结束符表示的是语句的结束,这在 ECMAScript 标准中称为限制产生式 (restricted production)。
通俗地说,在限制产生式中,解析器优先启用 ASI 机制。一个典型限制产生式的例子是 return
语句。
function test(){
return
{}
}
console.log(test()); // undefined
标准规定的其它限制产生式有:
1. 后缀 自增/自减运算符之前。
2. continue, break, return, throw, yield之后。
3. ES6 箭头函数(参数和箭头之间不能换行)。
所以我们要尽量避免在上述位置加换行符。