備胎的自我修養——趣談 JavaScript 中的 ASI (Automatic Semicolon Insertion)

圖片描述

什麼是 ASI ?

自動分號插入 (automatic semicolon insertion, ASI) 是一種程序解析技術,它在 JavaScript 程序的語法分析 (parsing) 階段起做用。javascript

根據 ES2015 規範,某些(不是所有) JavaScript 語句須要用 ; 來表示語句的結束;然而爲了方便書寫,在某些狀況下這些分號是能夠從源碼中省略的,此時咱們稱 ; 被 parser 自動插入到符號流 (token stream) 中,這種機制稱爲 ASI。html

所謂的「自動分號插入」其實只是一種形象的描述,parser 並無真的插入一個個分號。ASI 只是表示編譯器正確理解了程序員的意圖。聽起來就像編譯器對程序員說:「Hey,哥們!雖然這裏你沒寫分號,但我知道你想說這條語句已經結束了。」java

須要用 ; 來表示結束的語句是:git

  • 空語句
  • letconstimportexport 開頭的聲明語句
  • var 開頭的變量聲明語句
  • 表達式語句
  • debugger 語句
  • continue 語句
  • break 語句
  • return 語句
  • throw 語句

並非全部的 JavaScript 語句都須要用 ; 表示結束,例如:程序員

  • 塊語句
  • if 語句
  • try 語句

這些語句原本就不須要 ; 表示結束。github

舉例來講,函數聲明 (Function declaration, FD) 不須要以分號結束:正則表達式

function add10(num) {
    return num + 10;
} // I don't need a semicolon here

若是你多此一舉地在 FD 以後寫了一個 ;,它會被解析爲一條空語句。express

ASI 規則

ASI 是備胎(第二選擇)

編譯器不會優先啓用 ASI 機制。實際上,在遇到行結束符 (Line Terminator) 時,編譯器老是先試圖將行結束符分隔的符號流看成一條語句來解析(其實有少數幾個特例:returnthrowbreakcontinueyield++--,隨後會介紹),實在不符合正確語法的狀況下,纔會退而求其次,啓用 ASI 機制,將行結束符分隔的符號流看成兩條語句(俗稱,插入分號)。來看下面的例子:編程

var a = 0
var b = 1

這個簡單代碼段的符號流爲:數組

var   a   =   0   \n   var   b   =   1

parser 從左至右解析這個符號流,解析過程當中它遇到了換行符 LF ( \n , 行結束符之一)。它看起來這樣自言自語:「我遇到了一個換行符,讓我先試試去掉它,把這個代碼段看成一條語句試試!」

因而 parser 實際上先解析了這樣一條語句:

var a = 0 var b = 1
// Uncaught SyntaxError: Unexpected token var

很顯然這是一條有語法錯誤的語句,此路不通!

parser 說:「這個符號流若是看成一條語句的話,是有語法錯誤的!這該怎麼辦呢?我是否是要就此放棄、直接拋出語法錯誤呢?不!我但是要成爲海賊王的男人!我要啓用 ASI 機制試試。」

因而不折不撓的 parser 又解析了下面的語句:

var a = 0; var b = 1     // legal syntax

Bingo! 沒有 SyntaxError ,解析經過!

parser 因而得意地對程序員說:「Hey,哥們!雖然在 \n 前面你沒寫分號,但我知道你想說 var a = 0 這條賦值語句已經結束了!」

「高!實在是高!」

脆弱的符號、被誤解的源碼

須要注意的是,parser 對符號流的這種處理機制有時會致使它誤解程序員的意圖。

var a = [1, [2, 3]]
[3, 2, 1].map(function(num) {
    return num * num;
})

因爲 parser 老是優先將換行符先後的符號流看成一條語句解析,parser 實際上先解析了下面的語句:

var a = [1, [2, 3]][3, 2, 1].map(function(num) {
    return num * num;
})

這是一條語法正確的語句。它的含義是:先聲明變量a ,對 [1, [2, 3]][3, 2, 1] 求值以後獲得數組 [2, 3] ,對 [2, 3] 進行 (num) => num * num 映射操做獲得 [4, 9],將數組 [4, 9] 賦給變量 a

( 開始的語句,好比 IIFE ,也會致使程序被 parser 誤解。

(function fn() {
    return fn;
})()
(function() {
    console.log('我會顯示在控制檯嗎?');
})()

它等價於

// 一條函數連續調用語句
(function fn() {
    return fn;
})()(function() {
    console.log('我會顯示在控制檯嗎?');
})()  // => fn

/ 開始的語句,一般是正則表達式放在語句起始處(這種狀況比較少見),也會致使程序被 parser 誤解。

var a = 2
/error/i.test('error')

它等價於

var a = 2 / error / i.test('error')
// => Uncaught ReferenceError: error is not defined

須要注意的是,雖然 var a = 2 / error / i.test('error') 會拋出 ReferenceError 異常,但它是一條沒有語法錯誤 (SyntaxError) 的語句。換句話說,該語句在 parser 眼裏是一條語法正確的語句,所以 parser 不會啓用 ASI 機制。

語句起始處的 +- 也會致使源碼被誤解(更加少見)。

var num = 5
+new Date - new Date(2009, 10)

等價於

var num = 5 + new Date - new Date(2009, 10)

源碼的意圖被 parser 誤解,有兩個必要條件:

  1. parser 優先將行結束符先後的符號流按一條語句解析,這是 ECMAScript 標準的規定,全部 parser 必需要按此要求實現。
  2. 行結束符以後的符號 (token) 有二義性,使得該符號與上條語句可以無縫對接,不致使語法錯誤。

實際上,有二義性的符號原本就很少,能致使源碼意圖被改變的符號數來數去就只有 [(/+- 這五個而已。咱們能夠把它們理解成「脆弱的符號」,在它們前面顯式地加上防護性分號 (defensive semicolon) 來保護其含義不被改變。

限制產生式——備胎轉正

前文說到,ASI 是一種備用選擇。然而在 ECMAScript 中,有幾種特殊語句是不容許行結束符存在的。若是語句中有行結束符,parser 會優先認爲行結束符表示的是語句的結束,這在 ECMAScript 標準中稱爲限制產生式 (restricted production)。

通俗地說,在限制產生式中,parser 優先啓用 ASI 機制。

一個典型限制產生式的例子是 return 語句。

function a() {
    return
    {};
}
a()   // => undefined

按照通常解析規則,若是 ASI 是第二選擇,那麼 parser 優先忽略 \n ,該代碼段應與下面的程序無異:

function a() {
    return {};
}
a()  // => {} (empty object)

然而事實並不是如此,由於 ECMAScript 標準對合法的 return 語句作了以下限制:

ReturnStatement:
return [no LineTerminator here] Expression ;

return 語句中是不容許在 return 關鍵字以後出現行結束符的,因此上面的代碼段其實等價於:

function a() {
    return;    // ReturnStatement
    {}         // BlockStatement
    ;          // EmptyStatement
}
a()  // => undefined

函數體內的代碼被解析爲 return 語句、塊語句、空語句三條單獨的語句。

標準規定的其它限制產生式有:

  • continue 語句
  • break 語句
  • throw 語句
  • 箭頭函數 (箭頭左側不容許有行結束符)
  • yield 表達式
  • 後自增/自減表達式

這些狀況都不容許有換行符存在。

a
++
b

被解析爲

a;
++b;

ES2015 標準給出了關於限制產生式的編程建議:

  • A postfix ++ or -- operator should appear on the same line as its operand. (後自增運算符或後自減運算符應與它的操做數處於同一行。)
  • An Expression in a return or throw statement or an AssignmentExpression in a yield expression should start on the same line as the return, throw, or yield token. (returnthrow 語句中的表達式以及 yield 表達式中的賦值表達式應與 returnthrowyield 這些關鍵字處於同一行。)
  • An IdentifierReference in a break or continue statement should be on the same line as the break or continue token. (breakcontinue 語句中的標籤名應與 breakcontinue 關鍵字處於同一行。)

言而總之,總而言之,ES2015 標準這一節就告訴你一件事:在限制生產式中別換行,換行就自動插入分號。

for 循環與空語句——永不使用的備胎

ASI 不適用於 for 循環頭部,即 parser 不會在這裏自動插入分號。

var a = ['once', 'a', 'rebound,', 'always', 'a', 'rebound.']
var msg = ''
for (var i = 0, len = a.length
    i < len
    i++) {
    msg += a[i] + ' '
}
console.log(msg)

好吧,也許你但願 parser 在 a.length 後面和 i < len 後面自動爲你插入分號,補全這個 for 循環語句,可是 parser 不會在 for 循環的頭部啓用 ASI 機制。parser 首先嚐試按一條語句解析

(var i = 0, len = a.length \n i < len \n i++)

這個符號流,發現它並不是一條合法語句後,就直接拋出了語法錯誤 Uncaught SyntaxError: Unexpected Identifier,根本不嘗試補全分號。

因此 for 循環頭部的分號必需要顯式地寫出:

var a = ['once', 'a', 'rebound,', 'always', 'a', 'rebound.']
var msg = ''
for (var i = 0, len = a.length;
i < len;
i++) {
msg += a[i] + ' '
}
console.log(msg)
// => 'once a rebound, always a rebound.' (一朝爲備胎,永久爲備胎)

相似地,有特殊含義的空語句也不能夠省略分號:

function infiniteLoop() {
    while('a rebound is a rebound is a rebound')
}

此段代碼是不合法的語句,parser 會拋出語法錯誤 Uncaught SyntaxError: Unexpected token } 。這是由於循環體中做爲空語句而存在的 ; 不能省略。

// legal syntax
function infiniteLoop() {
    while('a rebound is a rebound is a rebound');
}
// it is true that a rebound is a rebound is a rebound (備胎就是備胎,這是真理。)

總結

ASI 機制的存在爲 JavaScript 程序員提供了一種選擇:你能夠省略源碼中的絕大部分 ; 而不影響程序的正確解析。與 IT 業界的 「Vim 和 Emacs 哪一個是更好的編輯器」 同樣,JavaScript 社區隔一段時間就會出現「該不應寫分號」這樣的觀點之爭。本文並非想證實哪一種觀點更好,而是關注 ASI 機制自己的一些有趣事實。即使是堅決的無分號黨,也不得不認可,有些分號是不能省略的。這些不能省略的分號有:

  • for 循環頭部的分號
  • 做爲空語句存在的分號
  • 以 5 個「脆弱符號」開頭的語句以前的分號 (嚴格來說,此處的分號不是必須的;由於除了使用分號,還能夠用各類 hack 方法,好比 void)

而對於堅決的分號黨,有一個事實也不得不認可,那就是你的程序中極可能有 99% 的分號都是多餘的!若是你想嘗試一下不寫分號,能夠按照下面的步驟:

  1. 刪掉你全部語句結尾處的分號
  2. 若是你的語句開頭是 [(,在它前面加一個分號。Over!

相關資料

  1. Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript. Item. 6
  2. ES2015 Spec Section 11.9.1: Rules of Automatic Semicolon Insertion
  3. An Open Letters to JavaScript Leaders Regarding Semicolons
  4. JavaScript Semicolon Insertion. Everything you need to know
  5. A Bit of Advice for the JavaScript Semicolon Haters
  6. Automatic semicolon insertion in JavaScript
相關文章
相關標籤/搜索