最近在清理 Pocket 的未讀列表,看到了 An Open Letter to JavaScript Leaders Regarding Semicolons 才知道了 JavaScript 的 ASI,一種自動插入分號的機制。由於我是 「省略分號風格」 的支持者,以前也碰到過一次由於忽略分號產生的問題,因此對此比較重視,也特地多看了幾份文檔,但越看內心越模糊。並非我記不住 ( 和 [ 前面記得加 ;
這種結論,而是以爲看過的幾篇文章跟 ECMAScript 標準描述的有點區別。直到最近反覆琢磨才忽然有了 「原來如此」 的想法,因而就有了此文。javascript
這篇文章會用 ECMAScript 標準的 ASI 定義來解釋它究竟是如何運做的,我會盡可能用平易近人的方法描述它,避免官方文檔的晦澀。但願你跟我同樣有收穫。掌握 ASI 並不可以讓你立刻解決手頭的問題,但能讓你成爲一個更好的 JavaScript 程序員。html
按照 ECMAScript 標準,一些 特定語句(statement) 必須以分號結尾。分號表明這段語句的終止。可是有時候爲了方便,這些分號是有能夠省略的。這種狀況下解釋器會本身判斷語句該在哪裏終止。這種行爲被叫作 「自動插入分號」,簡稱 ASI (Automatic Semicolon Insertion) 。實際上分號並無真的被插入,這只是個便於解釋的形象說法。java
這些特定的語句有:程序員
空語句express
let
json
const
數組
import
函數
export
post
變量賦值測試
表達式
debugger
continue
break
return
throw
下面這段是我 我的的理解,上的定義同時也表示:
全部這些語句中的分號都是能夠省略的。
除此以外其餘的語句有兩種狀況,一是不須要分號的(好比 if
和函數定義),二是分號不能省略的(好比 for
),稍後會詳細介紹。
那麼 ASI 如何知道在哪裏插入分號呢?它會按照一些規則去判斷。但在說規則以前,咱們先了解一下 JS 是如何解析代碼的。
解析器在解析代碼時,會把代碼分紅不少 token 。一個 token 至關於一小段有特定意義的語法片斷。看一個例子你就會明白:
var a = 12;
上面這段代碼能夠分紅四個 token :
var
關鍵字
a
標識符
=
運算符
12
數字
除此以外,(
,.
等都算 token ,這裏只是讓你有個大概的概念,好比 12
整個是一個 token ,而不是 1
和 2
。字符串同理。
解釋器在解析語句時會一個一個讀入 token 嘗試構成一個完整的語句 (statement),直到碰到特定狀況(好比語法規定的終止)纔會認爲這個語句結束了。記得上文提到的 變量賦值 這個語句必須以分號結尾麼?這個例子中的終止符就是分號。用 token 構成語句的過程相似於正則裏的貪婪匹配,解釋器老是試圖用盡量多的 token 構成語句。
接下來是重點:任意 token 之間均可以插入一個或多個換行符 (Line Terminator) ,這徹底不影響 JS 的解析,因此上面的代碼能夠寫成下面這樣(功能等價):
var a = // = 和 12 之間有兩個換行符 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
事實上這並非省略分號的風格的錯誤,而是開發者沒有理解 JS 解釋器的工做原理。若是你傾向省略分號的風格,那瞭解 ASI 是必修課。
ECMAScript 標準定義的 ASI 包括 三條規則 和 兩條例外。
三條規則是描述什麼時候該自動插入分號:
解析器從左往右解析代碼(讀入 token),當碰到一個不能構成合法語句的 token 時,它會在如下幾種狀況中在該 token 以前插入分號,此時這個不合羣的 token 被稱爲 offending token :
若是這個 token 跟上一個 token 之間有至少一個換行。
若是這個 token 是 }
。
若是 前一個 token 是 )
,它會試圖把前面的 token 理解成 do...while
語句並插入分號。
當解析到文件末尾發現語法仍是有問題,就會在文件末尾插入分號。
當解析時碰到 restricted production 的語法(好比 return
),而且在 restricted production 規定的 [no LineTerminator here]
的地方發現換行,那麼換行的地方就會被插入分號。
兩條例外表示,就算符合上述規則,若是分號會被解析成下面的樣子,它也不能被自動插入:
分號不能被解析成空語句。
分號不能被解析成 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 a; while(b) c
這個例子中解析到 token c
的時候就不對了。這裏面既沒有換行也沒有 }
,但 c
前面是 )
,因此解析器把以前的 token 組成一個語句,並判斷該語句是否是 do...while
,結果正好是的!因而插入分號變成 do a; while(b) ;
,最後給 c
加上分號,結束。最終結果爲:
do a; while (b) ; c;
簡單點說,do...while
後面的分號是會自動插入的。但若是其餘以 )
結尾的狀況就不行了。規則一(狀況三)就是爲 do...while
量身定作的。
return a
你必定知道 return
和返回值之間不能換行,由於上面代碼會解析成:
return; a;
但爲何不能換行?由於 return
語句就是一個 restricted production。這是什麼意思?它是一組有嚴格限定的語法的統稱,這些語法都是在某個地方不能換行的,不能換行的地方會被標註 [no LineTerminator here]
。
好比 ECMAScript 的 return
語法定義以下:
return [no LineTerminator here] Expression ;
這表示 return
跟表達式之間是不容許換行的(但後面的表達式內部能夠換行)。若是這個地方剛好有換行,ASI 就會自動插入分號,這就是規則三的含義。
剛纔咱們說了 restricted production 是一組語法的統稱,它一共包含下面幾個語法:
後綴的 ++
和 --
return
continue
break
throw
ES6 箭頭函數(參數和箭頭之間不能換行)
yield
這些不用死記,由於按照常規書寫習慣,幾乎沒人會這樣換行的。順帶一提,continue
和 break
後面是能夠接 label 的。但這不在本文討論範圍內,有興趣能夠本身探索。
a ++ b
解析器讀到 token ++
時發現語句不合法,由於後綴表達式是不容許換行的,換句話說,換行的都不是後綴表達式。因此它只能按照規則一(狀況一)在 ++
前面加上分號來結束語句 a
,而後繼續執行,由於前綴表達式並非 restricted production ,因此 ++
和 b
能夠組成一條語句,而後按照規則二在末尾加上分號。最終結果爲:
a ;++ b;
if (a) else b
解釋器解析到 token else
時發現不合法,原本按照規則一(狀況一),它在應該加上分號變成 if (a)\n;
,但這樣 ;
就變成空語句了,因此按照例外一,這個分號不能加。程序在 else
處拋異常結束。Node.js 的運行結果:
else b ^^^^ SyntaxError: Unexpected token else
for (a; b )
解析器讀到 token )
時發現不合法,原本換行能夠自動插入分號,但按照例外二,不能爲 for
頭部自動插入分號,因而程序在 )
處拋異常結束。Node.js 運行結果以下:
) ^ SyntaxError: Unexpected token )
咱們很難有辦法去測試 ASI 是否是如預期那樣工做的,只能看到代碼最終執行結果是對是錯。ASI 也沒有手動打開或關掉去對比結果。但咱們能夠經過對比解析器生成的 tree 是否一致來判斷 ASI 加的分號是否是跟咱們預期的一致。這點能夠用 Esprima 在線解析器 完成。
拿這段代碼舉例子:
do a; while(b) c
Esprima 解析的 Syntax 以下所示(不須要看懂,記住大概樣子就行):
{ "type": "Program", "body": [ { "type": "DoWhileStatement", "body": { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "a" } }, "test": { "type": "Identifier", "name": "b" } }, { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "c" } } ], "sourceType": "script" }
而後咱們把加上分號的版本輸入進去:
do a; while(b); c;
你會發現生成的 Syntax 是一致的。這說明解釋器對這兩段代碼解析過程是一致的,咱們並無加入任何多餘的分號。
而後試試這個有多餘分號的版本:
do a; while(b); c;; // 結尾多一個分號
Esprima 結果:
{ "type": "Program", "body": [ { "type": "DoWhileStatement", "body": { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "a" } }, "test": { "type": "Identifier", "name": "b" } }, { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "c" } }, { // 多出來一個空語句 "type": "EmptyStatement" } ], "sourceType": "script" }
你會發現多出來一條空語句,那麼這個分號就是多餘的。
若是看到這裏,相信你對 ASI 和 JS 的解析機制已經有所瞭解。也許你會想 「那我不再省略分號了」,那我建議你看看參考資料裏的連接。並且就個人經驗,即便是分號的堅持者,少數地方也會無心識地使用 ASI 。好比有時候忘了寫分號,或者寫迭代器中的單行函數時。下次我會說下對省略分號的風格的見解,和如何用 ESLint 保證代碼風格的一致性。
ECMAScript: ASI
ECMAScript 標準定義。本文的概念和不少例子徹底遵守它來寫的。但也強烈建議你本身看看。
JavaScript Semicolon Insertion Everything you need to know
關於 ASI 的解釋,略微學術化,講得很詳細,也很客觀。
An Open Letter to JavaScript Leaders Regarding Semicolons
NPM 做者對 ASI 和兩種風格的見解,這篇更注重我的觀點的表達。他是省略分號風格的傾向者。
Esprima: Parser一個在線 JS 解析器。你能夠輸入一些語句來看看 token 都是什麼。也能夠經過 Tree 的變化來測試加不加分號的影響。