关于 JavaScript 的分号

1,928 阅读6分钟

前言

这一阵子在阅读和学习**《Effective JavaScript》,虽然说书籍中的语法不是最新,甚至有些老旧。但其中的内置知识和我们值得注意考虑的关于 JS 提升效率的规则**,让我收获颇多,而我也每天在精炼总结,完善排版中。

这一篇阅读的是「第 6 条:了解分号插入的局限」,这一条规则解决了我多年(哎嘿嘿也就一两年)的疑虑 —— JavaScript 中的分号 ; 究竟有什么隐藏规则要点,究竟在什么情况下适合添加?

下面这篇文章就着这个疑问,开始讲述关于 JavaScript 分号问题。

分号插入

JavaScript 同其他静态编译语言不同,它是一个能便利于离开语句结束分号工作,使得结果变得更轻量和优雅。那么首先介绍一下 JavaScript 自动分号插入结束,它是造成分号添加与否模糊不清的关键因素之一。

JavaScript 自动分号插入技术是一种程序解析技术。能推断出某些上下文中省略的分号,然后有效地自动地将分号“插入”到程序中,ECMAScript 标准也指定了分号机制,因此可选分号可以在不同的 JavaScript 引擎之间移植

分号插入在解析时有其陷阱,JavaScript 语法对其也有额外的限制。因此我们需了解学会分号插入的三条规则,便能从删除不必要的分号痛苦中解脱出来。

第一条规则

分号仅在 } 标记之前、一个或多个换行之后和程序输入的结尾被插入。

也就是说,只能在一个代码块、一行或一段程序结束的地方省略分号,不能在连续的语句中省略分号。

以我个人的理解来说就是只能在三个地方省略分号,{} 代码块,一句语句的结束 一段程序代码文件的结束。而其实以往分号插入的作用也就是标志结束的地方,因此是要我们熟悉规则,就能大大让代码变得更优雅数据,且不增加编译的难度。

合法

function area(r) { r = +r; return Math.PI * r * r }

非法

function area(r) { r = +r return Match.PI * r * r }	// error

第二条规则

分号仅在随后的输入标记不能解析时插入。

也就是说,分号插入是一种错误矫正机制。我们总是要注意下一条语句的开始,从而发现能否合法地省略分号。

五个字符问题

有 5 个明确有问题的字符需要密切注意:([+-/。这些依赖于具体上下文,且都能作为一个表达运算符或上一条语句的前缀。如下例子:

()

a = b
(f());
// 等价于
a = b(f());

// 将被解析为两条独立语句
a = b
f()

[]

a = b
["r", "g", "b"].forEach(function (key) {
  background[key] = foreground[key] / 2;
})
// 等价于
a = b["r", "g", "b"].forEach(function (key) {
  background[key] = foreground[key] / 2;
})

// 将被解析为两条独立语句,一条赋值,一条数组 forEach 方法
a = b
["r", "g", "b"].forEach(function (key) {
  background[key] = foreground[key] / 2;
})

+-

a = b
+c;
// 等价于
a = b + c;

// 将被解析为两条独立语句,一条赋值,一条转为正整数。
a = b
+c

// - 如上

/

/ 字符在通常情况下是数字计算操作符「除法」,但在某些情况下,有特殊意义是正则表达式标记的开始字符。

a = b
/Error/ i.test(str) && fail();
// 等价于
a = b / Error / i.test(str) && fail();	// '/' 将会被解析为除法运算符

// 将被解析为两条独立语句
a = b
/Error/ i.test(str) && fail()

脚本连接问题

省略分号可能导致脚本连接问题,若每个文件可能由大量的函数调用表达式组成。当每个文件作为一个单独的程序加载时,分号能自动地插入到末尾,将函数调用转变为一条语句。

// file1.js
(function() {
  // ...
})()

// file2.js
(function() {
  // ...
})()

但当我们使用多个文件作为程序加载文件时,若我们省略了分号,结果将被解析为一条单独的语句。

(function() {
  // ...
})()(function() {
  // ...
})()

我们可以防御性地在每个文件前缀一个额外的分号以保护脚本免受粗心连接的影响。也就是说,如果文件最开始的语句以上述所有 5 个字符问题开头,则需做出以下解决方法。

// file1.js
;(function() {
  // ...
})()

// file2.js
;(function() {
  // ...
})()

这也就消除了我常看见某些大型插件包中 js 文件开头竟是一个 ; 的奇怪场景,原来的防止脚本连接的问题

总的以上来说,省略语句的分号不仅需要当心当前文件的下一个标记(字符问题),而且还需要当心脚本连接后可能出现语句之后的任一标记。

JavaScript 语法限制产生式

JavaScript 语法限制产生式**不允许在两个字符之间出现换行,因此会强制地插入分号。**如下例子:

return 
{ };
// 将被解析为 3 条单独的语句。
return;
{ };
;

换句话说,return 关键字后的换行会强制自动地插入分号,该代码例子被解析为不带参数的 return 语句,后接一个空的代码块和一个空语句。

除了 return 的语法限制生成式,还有以下其他的 JavaScript 语句限制生产式。

  • throw 语句
  • 带有显示标签的 breakcontinue 语句
  • 后置自增或自减运算符

第三条规则

分号不会作为分隔符在 for 循环空语句的头部或空循环体的 while 循环中被自动插入。

否则会导致 JavaScript 编译错误。

for 循环头部

意味着你需要在 for 循环头部显示地包含分号。

// 在 for 循环头部中,以换行代替分号,将导致解析错误。
for (let i = 0, total = 1	// parse error
     i < n
   	 i++) {
  total *= 1
}

空循环体的 while

在空循环体的 while 循环同样也需要显示分号。

function infiniteLoop() { while (true) }	// parse error
function infiniteLoop() { while (true); }	// 正确

总结

因此关于 JavaScript 的分号规则总结如下:

  • 分号仅在 } 标记之前、一个或多个换行之后和程序输入的结尾被插入。
  • 分号仅在紧接着的标记不能被解析的时候推导分号。
  • 在以 ([+-/ 字符开头的语句前绝不能省略分号。
  • 当脚本连接的时候,在脚本之间显式地插入分号。
  • returnthrowbreakcontinue++-- 地参数之前觉不能换行。
  • 分号不能作为 for 循环的头部或空语句的分隔符而被推导出。