JavaScript自动分号插入(ASI)的规则是什么?

598

首先,我可能应该问一下这是否与浏览器有关。

我读到过,如果发现无效的标记,但代码部分在该无效标记之前是有效的,则在换行符之前插入一个分号。

然而,通常引用分号插入引起的错误的常见示例是:

return
  _a+b;

...这似乎不遵循此规则,因为 _a 将是一个有效的标记。

另一方面,分解调用链按预期工作:

$('#myButton')
  .click(function(){alert("Hello!")});

有没有人能提供更详细的规则说明?


42
@Miles,只是不要使用你提供的失效链接,;-)。该链接为http://www.ecma-international.org/publications/standards/Ecma-262.htm - Zach Lysobey
6
请查看上述引用的PDF文件的第26页。 - ᴠɪɴᴄᴇɴᴛ
3
自动分号插入是 ECMAScript 解释器在语句末尾缺失分号的情况下自动插入分号的一种机制。这种机制存在于语法分析阶段,在代码执行前被解释器处理。它的目的是增强 ECMAScript 代码的可读性和易用性,但有时会导致意外行为的发生,因此应该谨慎使用。 - Bennett Brown
4
请参考第11.9节自动分号插入。 - Andrew Lam
2
链接到Living spec - Sebastian Simon
7个回答

593

首先你需要了解哪些语句会受到自动分号插入(也称为ASI)的影响:

  • 空语句
  • var语句
  • 表达式语句
  • do-while语句
  • continue语句
  • break语句
  • return语句
  • throw语句

ASI的具体规则在规范中有描述§11.9.1 自动分号插入规则

其中描述了三种情况:

  1. 当遇到不被语法允许的非法标记时,如果符合以下条件,会在其前插入分号:
  • 该标记与前一个标记之间至少隔了一个LineTerminator
  • 该标记是}

例如:

    { 1
    2 } 3

被转化为

    { 1
    ;2 ;} 3;

NumericLiteral 1符合第一个条件,它后面的标记是行终止符。
2符合第二个条件,它后面的标记是}

  1. 当遇到输入标记流的末尾且解析器无法将输入标记流解析为单个完整程序时,会自动在输入流的末尾插入一个分号。

例如:

    a = b
    ++c

被转换为:

    a = b;
    ++c;
  1. 当文法的某个产生式允许一个标记时,但该产生式是一个限制性产生式时,分号会在限制性标记之前自动插入。

限制性产生式:

    UpdateExpression :
        LeftHandSideExpression [no LineTerminator here] ++
        LeftHandSideExpression [no LineTerminator here] --
    
    ContinueStatement :
        continue ;
        continue [no LineTerminator here] LabelIdentifier ;
    
    BreakStatement :
        break ;
        break [no LineTerminator here] LabelIdentifier ;
    
    ReturnStatement :
        return ;
        return [no LineTerminator here] Expression ;
    
    ThrowStatement :
        throw [no LineTerminator here] Expression ; 

    ArrowFunction :
        ArrowParameters [no LineTerminator here] => ConciseBody

    YieldExpression :
        yield [no LineTerminator here] * AssignmentExpression
        yield [no LineTerminator here] AssignmentExpression

经典示例,使用 ReturnStatement

    return 
      "something";

被转换为

    return;
      "something";

4
#1:语法不允许的标记通常不是行终止符,除非你指的是来自#3的受限制产生式。我认为你应该省略括号。 #2:为了清晰起见,示例中难道不应该只显示在 ++c 后插入的部分吗? - Bergi
6
请注意,ASI 实际上不需要“插入分号”,只需在引擎解析器中终止语句即可。 - Aprillion
1
“输入流”是什么意思?是指“一行”吗?“输入标记流”使理解变得有些困难。 - nonopolarity
规格链接对其他人有效吗?它将我带到一个几乎空白的页面,上面有一个失效的链接。 - intcreator
请解释一下,根据这些规则,下面的例子“太极者无极而生”中的“a [LineBreak] = [LineBreak] 3”仍然有效。 - Nir O.
将“NumericLiteral 1符合第一个条件,接下来的标记是行终止符”更改为“NumericLiteral 2符合第一个条件,标记与前一个标记(此处为1)由一个LineTerminator分隔”。 - Timo

65
我对规范中的这3条规则不是很理解,希望有更通俗易懂的说明。以下是我从《JavaScript 权威指南》(第6版,David Flanagan, O'Reilly,2011)中了解到的内容:

引用:
JavaScript 并不会把每个换行符作为分号,它只有在无法解析代码时才会把换行符视为分号。
另外一段引文:针对下面的代码
var a
a
=
3 console.log(a)

JavaScript不会将第二个换行符视为分号,因为它可以继续解析更长的语句a = 3;

和:

有两个例外:当JavaScript无法解析第二行作为第一行语句的连续时,它会将换行符解释为分号。第一个例外涉及return、break和continue语句。

如果这些单词之后出现了换行符,JavaScript将始终将该换行符解释为分号。

第二种情况涉及++和--运算符。如果要使用这些运算符作为后缀运算符,则它们必须出现在应用于的表达式的同一行上。否则,换行符将被视为分号,并且++或--将被解析为前缀运算符,应用于其后面的代码。例如,请考虑以下代码:

x 
++ 
y

这句话的意思是 x; ++y;,而不是 x++; y

简单地说,一般情况下只要逻辑通顺,JavaScript 就会将代码看作是连续的,除了两种情况:(1) 在某些关键词后面,比如 returnbreakcontinue 等等;(2) 如果出现了新的一行,并且这一行以 ++-- 开始,那么 JavaScript 会在前一行末尾添加一个分号 ;

“只要逻辑通顺”的部分让人感觉像正则表达式里的贪婪匹配。

因此,对于有换行符的 return,JavaScript 解释器会插入一个分号:

(再次引用:如果这些关键词 [比如 return] 后面跟着一个换行符…… 那么 JavaScript 总是把这个换行符解释成分号)

正是因为这个原因,经典的例子——

return
{ 
  foo: 1
}

不会像预期的那样工作,因为JavaScript解释器会将其视为:

return;   // returning nothing
{
  foo: 1
}

return之后不能立即换行:

return { 
  foo: 1
}

为了使其正常工作。如果您遵循在任何语句后使用分号的规则,您可以自己插入一个 ;

return { 
  foo: 1
};

53
直接来自于ECMA-262第五版ECMAScript规范

7.9.1 自动分号插入规则

自动分号插入有三个基本规则:

  1. 当程序从左到右解析时,遇到一个不符合语法任何产生式的记号(称为“有问题的记号”),如果以下条件之一成立,则在有问题的记号前自动插入分号:
    • 有问题的记号与前面的记号至少被一个LineTerminator所分隔。
    • 有问题的记号是}
  2. 当程序从左到右解析时,遇到输入记号流的结尾并且解析器无法将输入记号流解析为单个完整的 ECMAScript Program时,则在输入流的结尾自动插入分号。
  3. 当程序从左到右解析时,遇到一个符合语法某些产生式的记号,但该产生式是一个“受限制的产生式”,而且该记号是在带有注释“[no LineTerminator here]”的受限制产生式内紧接着终结符或非终结符的第一个记号(因此这样的记号称为受限制的记号),并且受限制的记号与前面的记号至少被一个LineTerminator所分隔,则在受限制的记号前自动插入分号。

但是,对于上述规则还有一个额外的覆盖条件:如果自动插入分号后分号会被解析为空语句或者该分号将成为for语句头部的两个分号之一(参见12.6.3),则不会自动插入分号。


19

关于自动分号插入和var语句,请注意在使用var跨越多行时不要忘记逗号。昨天,有人在我的代码中发现了这个问题:

    var srcRecords = src.records
        srcIds = [];

代码运行了,但效果是srcIds声明和赋值变成了全局变量,因为前一行带有var的本地声明不再适用,所以该语句被认为已经完成,出现了自动分号插入。


4
我使用 jsLint 的原因就在于这种情况。 - Zach Lysobey
1
在您的代码编辑器中使用JsHint/Lint,立即获得反馈:) - dmi3y
5
当漏掉应该结束一行的逗号时,自动插入分号。这不是一个规则,更像是一个“陷阱”。 - Dexygen
1
我认为balupton是正确的,如果你写成:var srcRecords = src.records srcIds = [];在一行中忘记了逗号或者你写成"return a && b"并没有遗漏任何东西...但是在a之前换行会在return后插入一个自动分号,这是由ASI规则定义的... - Sebastian
2
这也会被使用严格模式所捕获,因为它是对未声明变量的赋值。 - harpo
4
我认为在每一行输入var (let, const) 的清晰度胜过于打出这些字需要消耗的不到一秒钟的时间。 - squidbe

10
我找到的关于JavaScript的自动分号插入最具上下文描述的内容来自一本关于解析器构建的书。
引用:
JavaScript的“自动分号插入”规则是奇怪的。其他语言假设大多数换行符都是有意义的,只有在多行语句中忽略少数换行符,而JS则假定相反。它将所有换行符视为无意义的空格,除非遇到解析错误。如果出现错误,它会返回并尝试将前一个换行符转换为分号,以获得语法上有效的内容。
他继续将其描述为代码异味
引用:
如果我详细介绍这个设计注释,甚至介绍所有这种想法是一个坏主意的各种方式,那么这个设计注释将变成设计长篇大论。这是一团糟。JavaScript是我知道的唯一一个许多样式指南要求每个语句后面都显式使用分号的语言,即使该语言理论上允许您省略它们。

3

2
JavaScript中的大多数语句和声明必须以分号结尾,然而,为了方便程序员(减少输入、风格偏好、减少代码噪音、降低入门门槛),在某些源文本位置可以省略分号,运行时会根据规范中设定的一组规则自动插入分号。
总体规则:如果分号将被解析为空语句,或者该分号将成为for循环头部的两个分号之一,则不会自动插入分号。
规则1:如果JavaScript解析器遇到一个标记,如果没有分号,它将不被允许存在,并且该标记与前一个标记之间有一个或多个行终止符(例如换行符)、一个闭合括号 } 或 do-while 循环的最终括号 ) 分隔,则会自动插入分号。
换句话说:对于可运行程序中始终需要终止语句的源文本位置,如果省略了语句终止符(;),则会自动插入。这条规则是ASI的核心。
规则2:如果省略分号会导致代码出现错误,则会自动插入分号。
如果源代码文本不是有效的脚本或模块,程序末尾将插入一个分号。换句话说,程序员可以省略程序中的最后一个分号。
规则3:
如果遇到一个标记,该标记通常允许在不存在分号的情况下使用,但存在于几个特殊源文本位置(“限制性产生式”)之一中,并且这些位置明确禁止出现行终止符以避免歧义,那么将自动插入分号。
禁止出现行终止符的限制性产生式包括:
- 在后缀++和后缀--之前(因此,在换行符之后的一元递增/递减运算符将作为前缀运算符绑定到以下(而不是上一个)语句) - 在continuebreakthrowreturnyield之后 - 在箭头函数参数列表之后 - 在异步函数声明和表达式、生成器函数声明和表达式及方法以及异步箭头函数中async关键字之后
该规范包含完整的细节(请点击此处),以及以下实用建议:
引用块: ECMAScript程序员的实用建议如下: - 后缀++或--运算符应与其操作数位于同一行。 - 返回语句、抛出语句中的表达式或yield表达式中的AssignmentExpression应从同一行开始。 - break或continue语句中的LabelIdentifier应与break或continue标记位于同一行。 - 箭头函数参数的结尾和箭头“=>”应在同一行。 - 异步函数或方法之前的async标记应与紧随其后的标记位于同一行。
这是关于该主题的最佳网络文章(链接)

ASI陷阱示例

以 `(` 开头的行

开括号字符有多重含义。它可以表示一个表达式,也可以表示一个调用(与闭括号配对时)。

例如,以下代码会抛出 "Uncaught TypeError: console.log(...) is not a function" 错误,因为运行时尝试调用 console.log('bar') 的返回值:

let a = 'foo'
console.log('bar')
(a = 'bam') 

如果你通常省略分号,解决这个问题的方法之一是加上分号,以使你的意图更加明确。
let a = 'foo'
console.log('bar')
;(a = 'bam') // note semicolon at start of line

以 `[` 开始一行

开头的方括号字符 ([) 有多种含义。它可以表示对象属性访问,也可以表示数组的字面声明(当与右括号配对时),或者表示数组析取。

例如,以下代码会抛出 "Uncaught TypeError: Cannot set properties of undefined (setting 'foo')" 的错误,因为运行时尝试在 console.log('bar') 的响应上设置名为 'foo' 的属性值:

let a = 'foo'
console.log('bar')
[a] = ['bam']

一种解决办法是,如果您通常省略分号,可以加上一个分号以使您的意图明确无误:
let a = 'foo'
console.log('bar')
;[a] = ['bam'] // note semicolon at start of line

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接