摘要:行結束符之后的符號有二義性,使得該符號與上條語句能夠無縫對接,不導致語法錯誤。然而在中,有幾種特殊語句是不允許行結束符存在的。如果語句中有行結束符,會優先認為行結束符表示的是語句的結束,這在標準中稱為限制產生式。
什么是 ASI ?
自動分號插入 (automatic semicolon insertion, ASI) 是一種程序解析技術,它在 JavaScript 程序的語法分析 (parsing) 階段起作用。
根據 ES2015 規范,某些(不是全部) JavaScript 語句需要用 ; 來表示語句的結束;然而為了方便書寫,在某些情況下這些分號是可以從源碼中省略的,此時我們稱 ; 被 parser 自動插入到符號流 (token stream) 中,這種機制稱為 ASI。
所謂的“自動分號插入”其實只是一種形象的描述,parser 并沒有真的插入一個個分號。ASI 只是表示編譯器正確理解了程序員的意圖。聽起來就像編譯器對程序員說:“Hey,哥們!雖然這里你沒寫分號,但我知道你想說這條語句已經結束了?!?/p>
需要用 ; 來表示結束的語句是:
空語句
以 let 、 const 、import 、 export 開頭的聲明語句
以 var 開頭的變量聲明語句
表達式語句
debugger 語句
continue 語句
break 語句
return 語句
throw 語句
并不是所有的 JavaScript 語句都需要用 ; 表示結束,例如:
塊語句
if 語句
try 語句
這些語句本來就不需要 ; 表示結束。
舉例來說,函數聲明 (Function declaration, FD) 不需要以分號結束:
function add10(num) { return num + 10; } // I don"t need a semicolon here
如果你畫蛇添足地在 FD 之后寫了一個 ;,它會被解析為一條空語句。
ASI 規則 ASI 是備胎(第二選擇)編譯器不會優先啟用 ASI 機制。實際上,在遇到行結束符 (Line Terminator) 時,編譯器總是先試圖將行結束符分隔的符號流當作一條語句來解析(其實有少數幾個特例:return、throw、break、continue、yield 、++ 、 --,隨后會介紹),實在不符合正確語法的情況下,才會退而求其次,啟用 ASI 機制,將行結束符分隔的符號流當作兩條語句(俗稱,插入分號)。來看下面的例子:
var a = 0 var b = 1
這個簡單代碼段的符號流為:
var a = 0 var b = 1
parser 從左至右解析這個符號流,解析過程中它遇到了換行符 LF ( , 行結束符之一)。它看起來這樣自言自語:“我遇到了一個換行符,讓我先試試去掉它,把這個代碼段當作一條語句試試!”
于是 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,哥們!雖然在 前面你沒寫分號,但我知道你想說 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 誤解,有兩個必要條件:
parser 優先將行結束符前后的符號流按一條語句解析,這是 ECMAScript 標準的規定,所有 parser 必須要按此要求實現。
行結束符之后的符號 (token) 有二義性,使得該符號與上條語句能夠無縫對接,不導致語法錯誤。
實際上,有二義性的符號本來就不多,能導致源碼意圖被改變的符號數來數去就只有 [ 、( 、/ 、+ 、- 這五個而已。我們可以把它們理解成“脆弱的符號”,在它們前面顯式地加上防御性分號 (defensive semicolon) 來保護其含義不被改變。
限制產生式——備胎轉正前文說到,ASI 是一種備用選擇。然而在 ECMAScript 中,有幾種特殊語句是不允許行結束符存在的。如果語句中有行結束符,parser 會優先認為行結束符表示的是語句的結束,這在 ECMAScript 標準中稱為限制產生式 (restricted production)。
通俗地說,在限制產生式中,parser 優先啟用 ASI 機制。
一個典型限制產生式的例子是 return 語句。
function a() { return {}; } a() // => undefined
按照一般解析規則,如果 ASI 是第二選擇,那么 parser 優先忽略 ,該代碼段應與下面的程序無異:
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. (return 或 throw 語句中的表達式以及 yield 表達式中的賦值表達式應與 return 、throw 、yield 這些關鍵字處于同一行。)
An IdentifierReference in a break or continue statement should be on the same line as the break or continue token. (break 或 continue 語句中的標簽名應與 break 或 continue 關鍵字處于同一行。)
言而總之,總而言之,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 i < len 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% 的分號都是多余的!如果你想嘗試一下不寫分號,可以按照下面的步驟:
刪掉你所有語句結尾處的分號
如果你的語句開頭是 [ 或 (,在它前面加一個分號。Over!
相關資料Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript. Item. 6
ES2015 Spec Section 11.9.1: Rules of Automatic Semicolon Insertion
An Open Letters to JavaScript Leaders Regarding Semicolons
JavaScript Semicolon Insertion. Everything you need to know
A Bit of Advice for the JavaScript Semicolon Haters
Automatic semicolon insertion in JavaScript
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/85795.html
摘要:在以下的種情況是用回車或換行,是不會作自動插入分號來讓語句作結尾。以下情況必用分號。但有例外,賦值時可以加分號是對的語法。 起因 這個文章一開始回覆于這篇回答中: javascript初級問題 也有之前的朋友寫信來問,因為在讀到我個人寫的一本電子書: 從ES6開始的JavaScript學習生活,繁體,gitbook。我在寫作風格里有說明,這本電子書中的范例都是使用不用分號(;)作為代碼...
摘要:最近在清理的未讀列表,看到了才知道了的,一種自動插入分號的機制。這種行為被叫做自動插入分號,簡稱。不過在省略分號的風格中,這種解析特性會導致一些意外情況。規則標準定義的包括三條規則和兩條例外。規則一情況三就是為量身定做的。 TL;DR 最近在清理 Pocket 的未讀列表,看到了 An Open Letter to JavaScript Leaders Regarding Semico...
摘要:這段代碼工作正常,盡管沒有用分號在某些場景下是很管用的,特別是,有時候可以幫助減少代碼錯誤。比如不好的寫法盡管這段代碼能正常工作,但代碼中我們應盡量避免使用。前言 在我們平時工作中寫代碼是最頻繁的事情了,但我們的代碼真的好看嗎? 預計本文閱讀時間(10分鐘) 正文 1.1--語句結尾 我們來看一段代碼 //合法的代碼 var name = Dreams; function sayName(...
摘要:一行過長的代碼會影響閱讀體驗實際項目中,我們往往把過長的代碼分成多行去寫比如在中鏈經常要分成多行寫對此本人想到一個問題就是中在什么地方換行是合法并且不破壞原有代碼邏輯的探究這個問題過程中本人造了一個小玩具顧名思義它的作用就是在不破話代碼的邏 一行過長的代碼會影響閱讀體驗. 實際項目中,我們往往把過長的代碼分成多行去寫. 比如在js中, promise鏈經常要分成多行寫. 對此本人想到一...
摘要:中分號自動插入轉譯自鏈接描述在中,分號自動插入機制允許在一行代碼結尾省略分號。比如分號自動插入規則分號插入只是一個術語。如果在這些位置遇到換行了,分號將被插入。 JavaScript中分號自動插入 轉譯自:鏈接描述在JavaScript中,分號自動插入機制允許在一行代碼結尾省略分號。你應該養成一直書寫分號的習慣,與此同時掌握JavaScript分號省略處理機制是十分重要的。因為這不僅有...
閱讀 2003·2021-08-11 11:13
閱讀 1027·2021-07-25 21:37
閱讀 2583·2019-08-29 18:42
閱讀 2516·2019-08-26 12:18
閱讀 921·2019-08-26 11:29
閱讀 1695·2019-08-23 17:17
閱讀 2670·2019-08-23 15:55
閱讀 2612·2019-08-23 14:34