JavaScript ASI 機制詳解

TL;DR

最近在清理 Pocket 的未讀列表,看到了 An Open Letter to JavaScript Leaders Regarding Semicolons 才知道了 JavaScript 的 ASI,一種自動插入分號的機制。由於我是 「省略分號風格」 的支持者,以前也碰到過一次由於忽略分號產生的問題,因此對此比較重視,也特地多看了幾份文檔,但越看內心越模糊。並非我記不住 ( 和 [ 前面記得加 ; 這種結論,而是以爲看過的幾篇文章跟 ECMAScript 標準描述的有點區別。直到最近反覆琢磨才忽然有了 「原來如此」 的想法,因而就有了此文。javascript

這篇文章會用 ECMAScript 標準的 ASI 定義來解釋它究竟是如何運做的,我會盡可能用平易近人的方法描述它,避免官方文檔的晦澀。但願你跟我同樣有收穫。掌握 ASI 並不可以讓你立刻解決手頭的問題,但能讓你成爲一個更好的 JavaScript 程序員。html

什麼是 ASI

按照 ECMAScript 標準,一些 特定語句(statement) 必須以分號結尾。分號表明這段語句的終止。可是有時候爲了方便,這些分號是有能夠省略的。這種狀況下解釋器會本身判斷語句該在哪裏終止。這種行爲被叫作 「自動插入分號」,簡稱 ASI (Automatic Semicolon Insertion) 。實際上分號並無真的被插入,這只是個便於解釋的形象說法。java

這些特定的語句有:程序員

  • 空語句express

  • letjson

  • const數組

  • import函數

  • exportpost

  • 變量賦值測試

  • 表達式

  • debugger

  • continue

  • break

  • return

  • throw

下面這段是我 我的的理解,上的定義同時也表示:

  1. 全部這些語句中的分號都是能夠省略的。

  2. 除此以外其餘的語句有兩種狀況,一是不須要分號的(好比 if 和函數定義),二是分號不能省略的(好比 for),稍後會詳細介紹。

那麼 ASI 如何知道在哪裏插入分號呢?它會按照一些規則去判斷。但在說規則以前,咱們先了解一下 JS 是如何解析代碼的。

Token

解析器在解析代碼時,會把代碼分紅不少 token 。一個 token 至關於一小段有特定意義的語法片斷。看一個例子你就會明白:

var a = 12;

上面這段代碼能夠分紅四個 token :

  1. var 關鍵字

  2. a 標識符

  3. = 運算符

  4. 12 數字

除此以外,(. 等都算 token ,這裏只是讓你有個大概的概念,好比 12 整個是一個 token ,而不是 12。字符串同理。

解釋器在解析語句時會一個一個讀入 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 是必修課。

ASI 規則

ECMAScript 標準定義的 ASI 包括 三條規則兩條例外

三條規則是描述什麼時候該自動插入分號:

  1. 解析器從左往右解析代碼(讀入 token),當碰到一個不能構成合法語句的 token 時,它會在如下幾種狀況中在該 token 以前插入分號,此時這個不合羣的 token 被稱爲 offending token :

    • 若是這個 token 跟上一個 token 之間有至少一個換行。

    • 若是這個 token 是 }

    • 若是 前一個 token 是 ),它會試圖把前面的 token 理解成 do...while 語句並插入分號。

  2. 當解析到文件末尾發現語法仍是有問題,就會在文件末尾插入分號。

  3. 當解析時碰到 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

這個是爲了解釋規則一(狀況三),這是最繞的部分,代碼以下:

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

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

這些不用死記,由於按照常規書寫習慣,幾乎沒人會這樣換行的。順帶一提,continuebreak 後面是能夠接 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

for (a; b
)

解析器讀到 token ) 時發現不合法,原本換行能夠自動插入分號,但按照例外二,不能爲 for 頭部自動插入分號,因而程序在 ) 處拋異常結束。Node.js 運行結果以下:

)
^

SyntaxError: Unexpected token )

如何手動測試 ASI

咱們很難有辦法去測試 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 的變化來測試加不加分號的影響。

相關文章
相關標籤/搜索