• 究竟要不要写分号(语言风格)?

实际上,行尾使用分号的风格来自于 Java,也来自于 C 语言和 C++,这一设计最初是为了降低编译器的工作负担。

但是,从今天的角度来看,行尾使用分号其实是一种语法噪音,恰好 JavaScript 语言又提供了相对可用的分号自动补全规则,所以,很多 JavaScript 的程序员都是倾向于不写分号。

自动插入分号规则

自动插入分号规则其实独立于所有的语法产生式定义,它的规则说起来非常简单,只有三条。

  • 要有换行符,且下一个符号是不符合语法的,那么就尝试插入分号。
  • 有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。
  • 源代码结束处,不能形成完整的脚本或者模块结构,那么就自动插入分号。

这样描述是比较难以理解的,我们一起看一些实际的例子进行分析:

  1. let a = 1
  2. void function(a){
  3. console.log(a);
  4. }(a); // 1

在这个例子中,第一行的结尾处有换行符,接下来 void 关键字接在 1 之后是不合法的,这命中了我们的第一条规则,因此会在 void 前插入分号。

  1. var a = 1, b = 1, c = 1;
  2. a
  3. ++
  4. b
  5. ++
  6. c

这也是个著名的例子,我们看第二行的 a 之后,有换行符,后面遇到了 ++ 运算符,a 后面跟 ++ 是合法的语法,但是我们看看 JavaScript 标准定义中,有[no LineTerminator here]这个字样,这是一个语法定义中的规则,你可以感受一下这个规则的内容(下一小节,我会给你详细介绍 no LineTerminator here ):

  1. UpdateExpression[Yield, Await]:
  2. LeftHandSideExpression[?Yield, ?Await]
  3. LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]++
  4. LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]--
  5. ++UnaryExpression[?Yield, ?Await]
  6. --UnaryExpression[?Yield, ?Await]

于是,这里 a 的后面就要插入一个分号了。所以这段代码最终的结果,b 和 c 都变成了 2,而 a 还是 1。

  1. (function(a){
  2. console.log(a);
  3. })()
  4. (function(a){
  5. console.log(a);
  6. })()

这个例子是比较有实际价值的例子,这里两个 function 调用的写法被称作 IIFE(立即执行的函数表达式),是个常见技巧。

这段代码意图上显然是形成两个 IIFE。

我们来看第三行结束的位置,JavaScript 引擎会认为函数返回的可能是个函数,那么,在后面再跟括号形成函数调用就是合理的,因此这里不会自动插入分号。

这是一些鼓励不写分号的编码风格会要求大家写 IIFE 时必须在行首加分号的原因。

  1. function f(){
  2. return/*
  3. This is a return value.
  4. */1;
  5. }
  6. f();

在这个例子中,return 和 1 被用注释分隔开了。

根据 JavaScript 自动插入分号规则,带换行符的注释也被认为是有换行符,而恰好的是,return 也有[no LineTerminator here]规则的要求。所以这里会自动插入分号,f 执行的返回值是 undefined。

no LineTerminator here 规则

no LineTerminator here 规则表示它所在的结构中的这一位置不能插入换行符。

自动插入分号规则的第二条:有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。跟 no LineTerminator here 规则强相关,那么我们就找出 JavaScript 语法定义中的这些规则。
image.png
下面一段代码展示了,带标签的 continue 语句,不能在 continue 后插入换行。

  1. outer:for(var j = 0; j < 10; j++)
  2. for(var i = 0; i < j; i++)
  3. continue /*no LineTerminator here*/ outter

break 跟 continue 是一样的,break 后也不能插入换行:

  1. outer:for(var j = 0; j < 10; j++)
  2. for(var i = 0; i < j; i++)
  3. break /*no LineTerminator here*/ outter

我们前面已经提到过 return 和后自增、后自减运算符。

  1. function f(){
  2. return /*no LineTerminator here*/1;
  3. }
  1. i/*no LineTerminator here*/++
  2. i/*no LineTerminator here*/--

以及,throw 和 Exception 之间也不能插入换行符:

  1. throw/*no LineTerminator here*/new Exception("error")

凡是 async 关键字,后面都不能插入换行符:

  1. async/*no LineTerminator here*/function f(){
  2. }
  3. const f = async/*no LineTerminator here*/x => x*x

箭头函数的箭头前,也不能插入换行:

  1. const f = x/*no LineTerminator here*/=> x*x

yield 之后,不能插入换行:

  1. function *g(){
  2. var i = 0;
  3. while(true)
  4. yield/*no LineTerminator here*/i++;
  5. }

到这里,我已经整理了所有标准中的 no LineTerminator here 规则,实际上,no LineTerminator here 规则的存在,多数情况是为了保证自动插入分号行为是符合预期的,但是令人遗憾的是,JavaScript 在设计的最初,遗漏了一些重要的情况,所以有一些不符合预期的情况出现,需要我们格外注意。

不写分号需要注意的情况

以括号开头的语句

我们在前面的案例中,已经展示了一种情况,那就是以括号开头的语句:

  1. (function(a){
  2. console.log(a);
  3. })()/*这里没有被自动插入分号*/
  4. (function(a){
  5. console.log(a);
  6. })()

这段代码看似两个独立执行的函数表达式,但是其实第三组括号被理解为传参,导致抛出错误。

以数组开头的语句

除了括号,以数组开头的语句也十分危险:

  1. var a = [[]]/*这里没有被自动插入分号*/
  2. [3, 2, 1, 0].forEach(e => console.log(e))

这段代码本意是一个变量 a 赋值,然后对一个数组执行 forEach,但是因为没有自动插入分号,被理解为下标运算符和逗号表达式,我这个例子展示的情况,甚至不会抛出错误,这对于代码排查问题是个噩梦。

以正则表达式开头的语句

正则表达式开头的语句也值得你去多注意一下。我们来看这个例子。

  1. var x = 1, g = {test:()=>0}, b = 1/*这里没有被自动插入分号*/
  2. /(a)/g.test("abc")
  3. console.log(RegExp.$1)

这段代码本意是声明三个变量,然后测试一个字符串中是否含有字母 a,但是因为没有自动插入分号,正则的第一个斜杠被理解成了除号,后面的意思就都变了。

注意,我构造的这个例子跟上面的例子一样,同样不会抛错,凡是这一类情况,都非常致命。

以 Template 开头的语句

以 Template 开头的语句比较少见,但是跟正则配合时,仍然不是不可能出现:

  1. var f = function(){
  2. return "";
  3. }
  4. var g = f/*这里没有被自动插入分号*/
  5. `Template`.match(/(a)/);
  6. console.log(RegExp.$1)

这段代码本意是声明函数 f,然后赋值给 g,再测试 Template 中是否含有字母 a。但是因为没有自动插入分号,函数 f 被认为跟 Template 一体的,进而被莫名其妙地执行了一次。
🔚