自動分號插入 (automatic semicolon insertion, ASI) 是一種程序解析技術,它在 JavaScript 程序的語法分析 (parsing) 階段起做用。javascript
根據 ES2015 規範,某些(不是所有) JavaScript 語句須要用 ;
來表示語句的結束;然而爲了方便書寫,在某些狀況下這些分號是能夠從源碼中省略的,此時咱們稱 ;
被 parser 自動插入到符號流 (token stream) 中,這種機制稱爲 ASI。html
所謂的「自動分號插入」其實只是一種形象的描述,parser 並無真的插入一個個分號。ASI 只是表示編譯器正確理解了程序員的意圖。聽起來就像編譯器對程序員說:「Hey,哥們!雖然這裏你沒寫分號,但我知道你想說這條語句已經結束了。」java
須要用 ;
來表示結束的語句是:git
let
、 const
、import
、 export
開頭的聲明語句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 機制。實際上,在遇到行結束符 (Line Terminator) 時,編譯器老是先試圖將行結束符分隔的符號流看成一條語句來解析(其實有少數幾個特例:return
、throw
、break
、continue
、yield
、++
、 --
,隨後會介紹),實在不符合正確語法的狀況下,纔會退而求其次,啓用 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 誤解,有兩個必要條件:
實際上,有二義性的符號原本就很少,能致使源碼意圖被改變的符號數來數去就只有 [
、(
、/
、+
、-
這五個而已。咱們能夠把它們理解成「脆弱的符號」,在它們前面顯式地加上防護性分號 (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 標準給出了關於限制產生式的編程建議:
++
or --
operator should appear on the same line as its operand. (後自增運算符或後自減運算符應與它的操做數處於同一行。)return
or throw
statement or an AssignmentExpression in a yield
expression should start on the same line as the return
, throw
, or yield
token. (return
或 throw
語句中的表達式以及 yield
表達式中的賦值表達式應與 return
、throw
、yield
這些關鍵字處於同一行。)break
or continue
statement should be on the same line as the break
or continue
token. (break
或 continue
語句中的標籤名應與 break
或 continue
關鍵字處於同一行。)言而總之,總而言之,ES2015 標準這一節就告訴你一件事:在限制生產式中別換行,換行就自動插入分號。
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 機制自己的一些有趣事實。即使是堅決的無分號黨,也不得不認可,有些分號是不能省略的。這些不能省略的分號有:
而對於堅決的分號黨,有一個事實也不得不認可,那就是你的程序中極可能有 99% 的分號都是多餘的!若是你想嘗試一下不寫分號,能夠按照下面的步驟:
[
或 (
,在它前面加一個分號。Over!