# 第五章:文法
我們想要解決的最后一個主要話題是JavaScript的語法如何工作(也稱為它的文法)。你可能認為你懂得如何編寫JS,但是語言文法的各個部分中有太多微妙的地方導致了困惑和誤解,所以我們想要深入這些部分并搞清楚一些事情。
注意:?對于讀者們來說,“文法(grammar)”一詞不像“語法(syntax)”一詞那么為人熟知。在許多意義上,它們是相似的詞,描述語言如何工作的?*規則*。它們有一些微妙的不同,但是大部分對于我們在這里的討論無關緊要。JavaScript的文法是一種結構化的方式,來描述語法(操作符,關鍵字,等等)如何組合在一起形成結構良好,合法的程序。換句話說,拋開文法來討論語法將會忽略許多重要的細節。所以我們在本章中注目的內容的最準確的描述是?*文法*,盡管語言中的純語法才是開發者們直接交互的。
## 語句與表達式
一個很常見的現象是,開發者們假定“語句(statement)”和“表達式(expression)”是大致等價的。但是這里我們需要區分它們倆,因為在我們的JS程序中它們有一些非常重要的區別。
為了描述這種區別,讓我們借用一下你可能更熟悉的術語:英語。
一個“句子(sentence)”是一個表達想法的詞匯的完整構造。它由一個或多個“短語(phrase)”組成,它們每一個都可以用標點符號或連詞(“和”,“或”等等)連接。一個短語本身可以由更小的短語組成。一些短語是不完整的,而且本身沒有太多含義,而另一些短語可以自成一句。這些規則總體地稱為英語的?*文法*。
JavaScript文法也類似。語句就是句子,表達式就是短語,而操作符就是連詞/標點。
JS中的每一個表達式都可以被求值而成為一個單獨的,具體的結果值。舉例來說:
```source-js
var a = 3 * 6;
var b = a;
b;
```
在這個代碼段中,`3 * 6`是一個表達式(求值得值`18`)。而第二行的`a`也是一個表達式,第三行的`b`也一樣。對表達式`a`和`b`求值都會得到在那一時刻存儲在這些變量中的值,也就偶然是`18`。
另外,這三行的每一行都是一個包含表達式的語句。`var a = 3 * 6`和`var b = a`稱為“聲明語句(declaration statments)”因為它們每一個都聲明了一個變量(并選擇性地給它賦值)。賦值`a = 3 * 6`和`b = a`(除去`var`)被稱為賦值表達式(assignment expressions)。
第三行僅僅含有一個表達式`b`,但是它本身也是一個語句(雖然不是非常有趣的一個!)。這一般稱為一個“表達式語句(expression statement)”。
### 語句完成值
一個鮮為人知的事實是,所有語句都有完成值(即使這個值只是`undefined`)。
你要如何做才能看到一個語句的完成值呢?
最明顯的答案是把語句敲進你的瀏覽器開發者控制臺,因為當你運行它時,默認地控制臺會報告最近一次執行的語句的完成值。
讓我們考慮一下`var b = a`。這個語句的完成值是什么?
`b = a`賦值表達式給出的結果是被賦予的值(上面的`18`),但是`var`語句本身給出的結果是`undefined`。為什么?因為在語言規范中`var`語句就是這么定義的。如果你在你的控制臺中敲入`var a = 42`,你會看到`undefined`被報告而不是`42`。
注意:?技術上講,事情要比這復雜一些。在ES5語言規范,12.2部分的“變量語句”中,`VariableDeclaration`算法實際上返回了一個值(一個包含被聲明變量的名稱的`string`?—— 詭異吧!?),但是這個值基本上被`VariableStatement`算法吞掉了(除了在`for..in`循環中使用),而這強制產生一個空的(也就是`undefined`)完成值。
事實上,如果你曾在你的控制臺上(或者一個JavaScript環境的REPL —— read/evaluate/print/loop工具)做過很多的代碼實驗的話,你可能看到過許多不同的語句都報告`undefined`,而且你也許從來沒理解它是什么和為什么。簡單地說,控制臺僅僅報告語句的完成值。
但是控制臺打印出的完成值并不是我們可以在程序中使用的東西。那么我們該如何捕獲完成值呢?
這是個更加復雜的任務。在我們解釋?*如何*?之前,讓我們先探索一下?*為什么*?你想這樣做。
我們需要考慮其他類型的語句的完成值。例如,任何普通的`{ .. }`塊兒都有一個完成值,即它所包含的最后一個語句/表達式的完成值。
考慮如下代碼:
```source-js
var b;
if (true) {
b = 4 + 38;
}
```
如果你將這段代碼敲入你的控制臺/REPL,你可能會看到它報告`42`,因為`42`是`if`塊兒的完成值,它取自`if`的最后一個復制表達式語句`b = 4 + 38`。
換句話說,一個塊兒的完成值就像?*隱含地返回*?塊兒中最后一個語句的值。
注意:?這在概念上與CoffeeScript這樣的語言很類似,它們隱含地從`function`中`return`值,這些值與函數中最后一個語句的值是相同的。
但這里有一個明顯的問題。這樣的代碼是不工作的:
```source-js
var a, b;
a = if (true) {
b = 4 + 38;
};
```
我們不能以任何簡單的語法/文法來捕獲一個語句的完成值并將它賦值給另一個變量(至少是還不能!)。
那么,我們能做什么?
警告:?僅用于演示的目的 —— 不要實際地在你的真實代碼中做如下內容!
我們可以使用臭名昭著的`eval(..)`(有時讀成“evil”)函數來捕獲這個完成值。
```source-js
var a, b;
a = eval( "if (true) { b = 4 + 38; }" );
a; // 42
```
啊呀呀。這太難看了。但是這好用!而且它展示了語句的完成值是一個真實的東西,不僅僅是在控制臺中,還可以在我們的程序中被捕獲。
有一個稱為“do表達式”的ES7提案。這是它可能工作的方式:
```source-js
var a, b;
a = do {
if (true) {
b = 4 + 38;
}
};
a; // 42
```
`do { .. }`表達式執行一個塊兒(其中有一個或多個語句),這個塊兒中的最后一個語句的完成值將成為`do`表達式的完成值,它可以像展示的那樣被賦值給`a`。
這里的大意是能夠將語句作為表達式對待 —— 他們可以出現在其他語句內部 —— 而不必將它們包裝在一個內聯的函數表達式中,并實施一個明確的`return ..`。
到目前為止,語句的完成值不過是一些瑣碎的事情。不過隨著JS的進化它們的重要性可能會進一步提高,而且很有希望的是`do { .. }`表達式將會降低使用`eval(..)`這樣的東西的沖動。
警告:?重復我剛才的訓誡:避開`eval(..)`。真的。更多解釋參見本系列的?*作用域與閉包*?一書。
### 表達式副作用
大多數表達式沒有副作用。例如:
```source-js
var a = 2;
var b = a + 3;
```
表達式`a + 3`本身并沒有副作用,例如改變`a`。它有一個結果,就是`5`,而且這個結果在語句`b = a + 3`中被賦值給`b`。
一個最常見的(可能)帶有副作用的表達式的例子是函數調用表達式:
```source-js
function foo() {
a = a + 1;
}
var a = 1;
foo(); // 結果:`undefined`,副作用:改變 `a`
```
還有其他的副作用表達式。例如:
```source-js
var a = 42;
var b = a++;
```
表達式`a++`有兩個分離的行為。*首先*,它返回`a`的當前值,也就是`42`(然后它被賦值給`b`)。但?*接下來*,它改變`a`本身的值,將它增加1。
```source-js
var a = 42;
var b = a++;
a; // 43
b; // 42
```
許多開發者錯誤的認為`b`和`a`一樣擁有值`43`。這種困惑源自沒有完全考慮`++`操作符的副作用在?*什么時候*?發生。
`++`遞增操作符和`--`遞減操作符都是一元操作符(見第四章),它們既可以用于后綴(“后面”)位置也可用于前綴(“前面”)位置。
```source-js
var a = 42;
a++; // 42
a; // 43
++a; // 44
a; // 44
```
當`++`像`++a`這樣用于前綴位置時,它的副作用(遞增`a`)發生在值從表達式中返回?*之前*,而不是`a++`那樣發生在?*之后*。
注意:?你認為`++a++`是一個合法的語法嗎?如果你試一下,你將會得到一個`ReferenceError`錯誤,但為什么?因為有副作用的操作符?要求一個變量引用?來作為它們副作用的目標。對于`++a++`來說,`a++`這部分會首先被求值(因為操作符優先級 —— 參見下面的討論),它會給出`a`在遞增?*之前*?的值。但然后它試著對`++42`求值,這將(如果你試一下)會給出相同的`ReferenceError`錯誤,因為`++`不能直接在`42`這樣的值上施加副作用。
有時它會被錯誤地認為,你可以通過將`a++`包進一個`( )`中來封裝它的?*后*?副作用,比如:
```source-js
var a = 42;
var b = (a++);
a; // 43
b; // 42
```
不幸的是,`( )`本身不會像我們希望的那樣,定義一個新的被包裝的表達式,而它會在`a++`表達式的?*后副作用*?之?*后*?求值。事實上,就算它能,`a++`也會首先返回`42`,而且除非你有另一個表達式在`++`的副作用之后對`a`再次求值,你也不會從這個表達式中得到`43`,于是`b`不會被賦值為`43`。
雖然,有另一種選擇:`,`語句序列逗號操作符。這個操作符允許你將多個獨立的表達式語句連成一個單獨的語句:
```source-js
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
```
注意:?`a++, a`周圍的`( .. )`是必需的。其原因的操作符優先級,我們將在本章后面討論。
表達式`a++, a`意味著第二個`a`語句表達式會在第一個`a++`語句表達式的?*后副作用*?之?*后*?進行求值,這表明它為`b`的賦值返回`43`。
另一個副作用操作符的例子是`delete`。正如我們在第二章中展示的,`delete`用于從一個`object`或一個`array`值槽中移除一個屬性。但它經常作為一個獨立語句被調用:
```source-js
var obj = {
a: 42
};
obj.a; // 42
delete obj.a; // true
obj.a; // undefined
```
如果被請求的操作是合法/可允許的,`delete`操作符的結果值為`true`,否則結果為`false`。但是這個操作符的副作用是它移除了屬性(或數組值槽)。
注意:?我們說合法/可允許是什么意思?不存在的屬性,或存在且可配置的屬性(見本系列?*this與對象原型*?的第三章)將會從`delete`操作符中返回`true`。否則,其結果將是`false`或者一個錯誤。
副作用操作符的最后一個例子,可能既是明顯的也是不明顯的,是`=`賦值操作符。
考慮如下代碼:
```source-js
var a;
a = 42; // 42
a; // 42
```
對于這個表達式來說,`a = 42`中的`=`看起來似乎不是一個副作用操作符。但如果我們檢視語句`a = 42`的結果值,會發現它就是剛剛被賦予的值(`42`),所以向`a`賦予的相同的值實質上是一種副作用。
提示:?相同的原因也適用于`+=`,`-=`這樣的復合賦值操作符的副作用。例如,`a = b += 2`被處理為首先進行`b += 2`(也就是`b = b + 2`),然后這個賦值的結果被賦予`a`。
這種賦值表達式(語句)得出被賦予的值的行為,主要在鏈式賦值上十分有用,就像這樣:
```source-js
var a, b, c;
a = b = c = 42;
```
這里,`c = 42`被求值得出`42`(帶有將`42`賦值給`c`的副作用),然后`b = 42`被求值得出`42`(帶有將`42`賦值給`b`的副作用),而最后`a = 42`被求值(帶有將`42`賦值給`a`的副作用)。
警告:?一個開發者們常犯的錯誤是將鏈式賦值寫成`var a = b = 42`這樣。雖然這看起來是相同的東西,但它不是。如果這個語句發生在沒有另外分離的`var b`(在作用域的某處)來正式聲明它的情況下,那么`var a = b = 42`將不會直接聲明`b`。根據`strict`模式的狀態,它要么拋出一個錯誤,要么無意中創建一個全局變量(參見本系列的?*作用域與閉包*)。
另一個要考慮的場景是:
```source-js
function vowels(str) {
var matches;
if (str) {
// 找出所有的元音字母
matches = str.match( /[aeiou]/g );
if (matches) {
return matches;
}
}
}
vowels( "Hello World" ); // ["e","o","o"]
```
這可以工作,而且許多開發者喜歡這么做。但是使用一個我們可以利用賦值副作用的慣用法,可以通過將兩個`if`語句組合為一個來進行簡化:
```source-js
function vowels(str) {
var matches;
// 找出所有的元音字母
if (str && (matches = str.match( /[aeiou]/g ))) {
return matches;
}
}
vowels( "Hello World" ); // ["e","o","o"]
```
注意:?`matches = str.match..`周圍的`( .. )`是必需的。其原因是操作符優先級,我們將在本章稍后的“操作符優先級”一節中討論。
我偏好這種短一些的風格,因為我認為它明白地表示了兩個條件其實是有關聯的,而非分離的。但是與大多數JS中的風格選擇一樣,哪一種?*更好*?純粹是個人意見。
### 上下文規則
在JavaScript文法規則中有好幾個地方,同樣的語法根據它們被使用的地方/方式不同意味著不同的東西。這樣的東西可能,孤立的看,導致相當多的困惑。
我們不會在這里詳盡地羅列所有這些情況,而只是指出常見的幾個。
#### `{ .. }`?大括號
在你的代碼中一對`{ .. }`大括號將主要出現在兩種地方(隨著JS的進化會有更多!)。讓我們來看看它們每一種。
##### 對象字面量
首先,作為一個`object`字面量:
```source-js
// 假定有一個函數`bar()`的定義
var a = {
foo: bar()
};
```
我們怎么知道這是一個`object`字面量?因為`{ .. }`是一個被賦予給`a`的值。
注意:?`a`這個引用被稱為一個“l-值”(也稱為左手邊的值)因為它是賦值的目標。`{ .. }`是一個“r-值”(也稱為右手邊的值)因為它僅被作為一個值使用(在這里作為賦值的源)。
##### 標簽
如果我們移除上面代碼的`var a =`部分會發生什么?
```source-js
// 假定有一個函數`bar()`的定義
{
foo: bar()
}
```
許多開發者臆測`{ .. }`只是一個獨立的沒有被賦值給任何地方的`object`字面量。但事實上完全不同。
這里,`{ .. }`只是一個普通的代碼塊兒。在JavaScript中擁有一個這樣的獨立`{ .. }`塊兒并不是一個很慣用的形式(在其他語言中要常見得多!),但它是完美合法的JS文法。當與`let`塊兒作用域聲明組合使用時非常有用(見本系列的?*作用域與閉包*)。
這里的`{ .. }`代碼塊兒在功能上差不多與附著在一些語句后面的代碼塊兒是相同的,比如`for`/`while`循環,`if`條件,等等。
但如果它是一個一般代碼塊兒,那么那個看起來異乎尋常的`foo: bar()`語法是什么?它怎么會是合法的呢?
這是因為一個鮮為人知的(而且,坦白地說,不鼓勵使用的)稱為“打標簽的語句”的JavaScript特性。`foo`是語句`bar()`(這個語句省略了末尾的`;`—— 見本章稍后的“自動分號”)的標簽。但一個打了標簽的語句有何意義?
如果JavaScript有一個`goto`語句,那么在理論上你就可以說`goto foo`并使程序的執行跳轉到代碼中的那個位置。`goto`通常被認為是一種糟糕的編碼慣用形式,因為它們使代碼更難于理解(也稱為“面條代碼”),所以JavaScript沒有一般的`goto`語句是一件?*非常好的事情*。
然而,JS的確支持一種有限的,特殊形式的`goto`:標簽跳轉。`continue`和`break`語句都可以選擇性地接受一個指定的標簽,在這種情況下程序流會有些像`goto`一樣“跳轉”。考慮一下代碼:
```source-js
// 用`foo`標記的循環
foo: for (var i=0; i<4; i++) {
for (var j=0; j<4; j++) {
// 每當循環相遇,就繼續外層循環
if (j == i) {
// 跳到被`foo`標記的循環的下一次迭代
continue foo;
}
// 跳過奇數的乘積
if ((j * i) % 2 == 1) {
// 內層循環的普通(沒有被標記的) `continue`
continue;
}
console.log( i, j );
}
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2
```
注意:?`continue foo`不意味著“走到標記為‘foo’的位置并繼續”,而是,“繼續標記為‘foo’的循環,并進行下一次迭代”。所以,它不是一個?*真正的*?隨意的`goto`。
如你所見,我們跳過了乘積為奇數的`3 1`迭代,而且被打了標簽的循環跳轉還跳過了`1 1`和`2 2`的迭代。
也許標簽跳轉的一個稍稍更有用的形式是,使用`break __`從一個內部循環里面跳出外部循環。沒有帶標簽的`break`,同樣的邏輯有時寫起來非常尷尬:
```source-js
// 用`foo`標記的循環
foo: for (var i=0; i<4; i++) {
for (var j=0; j<4; j++) {
if ((i * j) >= 3) {
console.log( "stopping!", i, j );
// 跳出被`foo`標記的循環
break foo;
}
console.log( i, j );
}
}
// 0 0
// 0 1
// 0 2
// 0 3
// 1 0
// 1 1
// 1 2
// stopping! 1 3
```
注意:?`break foo`不意味著“走到‘foo’標記的位置并繼續”,而是,“跳出標記為‘foo’的循環/代碼塊兒,并繼續它?*后面*?的部分”。不是一個傳統意義上的`goto`,對吧?
對于上面的問題,使用不帶標簽的`break`將可能會牽連一個或多個函數,共享作用域中變量的訪問,等等。它很可能要比帶標簽的`break`更令人糊涂,所以在這里使用帶標簽的`break`也許是更好的選擇。
一個標簽也可以用于一個非循環的塊兒,但只有`break`可以引用這樣的非循環標簽。你可以使用帶標簽的`break ___`跳出任何被標記的塊兒,但你不能`continue ___`一個非循環標簽,也不能用一個不帶標簽的`break`跳出一個塊兒。
```source-js
function foo() {
// 用`bar`標記的塊兒
bar: {
console.log( "Hello" );
break bar;
console.log( "never runs" );
}
console.log( "World" );
}
foo();
// Hello
// World
```
帶標簽的循環/塊兒極不常見,而且經常使人皺眉頭。最好盡可能地避開它們;比如使用函數調用取代循環跳轉。但是也許在一些有限的情況下它們會有用。如果你打算使用標簽跳轉,那么就確保使用大量注釋在文檔中記下你在做什么!
一個很常見的想法是,JSON是一個JS的恰當子集,所以一個JSON字符串(比如`{"a":42}`?—— 注意屬性名周圍的引號是JSON必需的!)被認為是一個合法的JavaScript程序。不是這樣的!?如果你試著把`{"a":42}`敲進你的JS控制臺,你會得到一個錯誤。
這是因為語句標簽周圍不能有引號,所以`"a"`不是一個合法的標簽,因此`:`不能出現在它后面。
所以,JSON確實是JS語法的子集,但是JSON本身不是合法的JS文法。
按照這個路線產生的一個極其常見的誤解是,如果你將一個JS文件加載進一個`<script src=..>`標簽,而它里面僅含有JSON內容的話(就像從API調用中得到那樣),這些數據將作為合法的JavaScript被讀取,但只是不能從程序中訪問。JSON-P(將JSON數據包進一個函數調用的做法,比如`foo({"a":42})`)經常被說成是解決了這種不可訪問性,通過向你程序中的一個函數發送這些值。
不是這樣的!?實際上完全合法的JSON值`{"a":42}`本身將會拋出一個JS錯誤,因為它被翻譯為一個帶有非法標簽的語句塊兒。但是`foo({"a":42})`是一個合法的JS,因為在它里面,`{"a":42}`是一個被傳入`foo(..)`的`object`字面量值。所以,更合適的說法是,JSON-P使JSON成為合法的JS文法!
##### 塊兒
另一個常為人所詬病的JS坑(與強制轉換有關 —— 見第四章)是:
```source-js
[] + {}; // "[object Object]"
{} + []; // 0
```
這看起來暗示著`+`操作符會根據第一個操作數是`[]`還是`{}`而給出不同的結果。但實際上這與它一點兒關系都沒有!
在第一行中,`{}`出現在`+`操作符的表達式中,因此被翻譯為一個實際的值(一個空`object`)。第四章解釋過,`[]`被強制轉換為`""`因此`{}`也會被強制轉換為一個`string`:`"[object Object]"`。
但在第二行中,`{}`被翻譯為一個獨立的`{}`空代碼塊兒(它什么也不做)。塊兒不需要分號來終結它們,所以這里缺少分號不是一個問題。最終,`+ []`是一個將`[]`*明確強制轉換*?為`number`的表達式,而它的值是`0`。
##### 對象解構
從ES6開始,你將看到`{ .. }`出現的另一個地方是“解構賦值”(更多信息參見本系列的?*ES6與未來*),確切地說是`object`解構。考慮下面的代碼:
```source-js
function getData() {
// ..
return {
a: 42,
b: "foo"
};
}
var { a, b } = getData();
console.log( a, b ); // 42 "foo"
```
正如你可能看出來的,`var { a , b } = ..`是ES6解構賦值的一種形式,它大體等價于:
```source-js
var res = getData();
var a = res.a;
var b = res.b;
```
注意:?`{ a, b }`?實際上是`{ a: a, b: b }`的ES6解構縮寫,兩者都能工作,但是人們期望短一些的`{ a, b }`能成為首選的形式。
使用一個`{ .. }`進行對象解構也可用于被命名的函數參數,這時它是同種類的隱含對象屬性賦值的語法糖:
```source-js
function foo({ a, b, c }) {
// 不再需要:
// var a = obj.a, b = obj.b, c = obj.c
console.log( a, b, c );
}
foo( {
c: [1,2,3],
a: 42,
b: "foo"
} ); // 42 "foo" [1, 2, 3]
```
所以,我們使用`{ .. }`的上下文環境整體上決定了它們的含義,這展示了語法和文法之間的區別。理解這些微妙之處以回避JS引擎進行意外的翻譯是很重要的。
#### `else if`?和可選塊兒
一個常見的誤解是JavaScript擁有一個`else if`子句,因為你可以這么做:
```source-js
if (a) {
// ..
}
else if (b) {
// ..
}
else {
// ..
}
```
但是這里有一個JS文法隱藏的性質:它沒有`else if`。但是如果附著在`if`和`else`語句后面的代碼塊兒僅包含一個語句時,`if`和`else`語句允許省略這些代碼塊兒周圍的`{ }`。毫無疑問,你以前已經見過這種現象很多次了:
```source-js
if (a) doSomething( a );
```
許多JS編碼風格指引堅持認為,你應當總是在一個單獨的語句塊兒周圍使用`{ }`,就像:
```source-js
if (a) { doSomething( a ); }
```
然而,完全相同的文法規則也適用于`else`子句,所以你經常編寫的`else if`形式?*實際上*?被解析為:
```source-js
if (a) {
// ..
}
else {
if (b) {
// ..
}
else {
// ..
}
}
```
`if (b) { .. } else { .. }`是一個緊隨著`else`的單獨的語句,所以你在它周圍放不放一個`{ }`都可以。換句話說,當你使用`else if`的時候,從技術上講你就打破了那個常見的編碼風格指導的規則,而且只是用一個單獨的`if`語句定義了你的`else`。
當然,`else if`慣用法極其常見,而且減少了一級縮進,所以它很吸引人。無論你用哪種方式,就在你自己的編碼風格指導/規則中明確地指出它,并且不要臆測`else if`是直接的文法規則。
## 操作符優先級
就像我們在第四章中講解的,JavaScript版本的`&&`和`||`很有趣,因為它們選擇并返回它們的操作數之一,而不是僅僅得出`true`或`false`的結果。如果只有兩個操作數和一個操作符,這很容易推理。
```source-js
var a = 42;
var b = "foo";
a && b; // "foo"
a || b; // 42
```
但是如果牽扯到兩個操作符,和三個操作數呢?
```source-js
var a = 42;
var b = "foo";
var c = [1,2,3];
a && b || c; // ???
a || b && c; // ???
```
要明白這些表達式產生什么結果,我們就需要理解當在一個表達式中有多于一個操作符時,什么樣的規則統治著操作符被處理的方式。
這些規則稱為“操作符優先級”。
我打賭大多數讀者都覺得自己已經很好地理解了操作符優先級。但是和我們在本系列叢書中講解的其他一切東西一樣,我們將撥弄這種理解來看看它到底有多扎實,并希望能在這個過程中學到一些新東西。
回想上面的例子:
```source-js
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
```
要是我們移除了`( )`會怎樣?
```source-js
var a = 42, b;
b = a++, a;
a; // 43
b; // 42
```
等一下!為什么這改變了賦給`b`的值?
因為`,`操作符要比`=`操作符的優先級低。所以,`b = a++, a`被翻譯為`(b = a++), a`。因為(如我們前面講解的)`a++`擁有?*后副作用*,賦值給`b`的值就是在`++`改變`a`之前的值`42`。
這只是為了理解操作符優先級所需的一個簡單事實。如果你將要把`,`作為一個語句序列操作符使用,那么知道它實際上擁有最低的優先級是很重要的。任何其他的操作符都將要比`,`結合得更緊密。
現在,回想上面的這個例子:
```source-js
if (str && (matches = str.match( /[aeiou]/g ))) {
// ..
}
```
我們說過賦值語句周圍的`( )`是必須的,但為什么?因為`&&`擁有的優先級比`=`更高,所以如果沒有`( )`來強制結合,這個表達式將被作為`(str && matches) = str.match..`對待。但是這將是個錯誤,因為`(str && matches)`的結果將不是一個變量(在這里是`undefined`),而是一個值,因此它不能成為`=`賦值的左邊!
好了,那么你可能認為你已經搞定操作符優先級了。
讓我們移動到更復雜的例子(在本章下面幾節中我們將一直使用這個例子),來?*真正*?測試一下你的理解:
```source-js
var a = 42;
var b = "foo";
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // ??
```
好的,邪惡,我承認。沒有人會寫這樣的表達式串,對吧?*也許*?不會,但是我們將使用它來檢視將多個操作符鏈接在一起時的各種問題,而鏈接多個操作符是一個非常常見的任務。
上面的結果是`42`。但是這根本沒意思,除非我們自己能搞清楚這個答案,而不是將它插進JS程序來讓JavaScript搞定它。
讓我們深入挖掘一下。
第一個問題 —— 你可能還從來沒問過 —— 是,第一個部分(`a && b || c`)是像`(a && b) || c`那樣動作,還是像`a && (b || c)`那樣動作?你能確定嗎?你能說服你自己它們實際上是不同的嗎?
```source-js
(false && true) || true; // true
false && (true || true); // false
```
那么,這就是它們不同的證據。但是`false && true || true`到底是如何動作的?答案是:
```source-js
false && true || true; // true
(false && true) || true; // true
```
那么我們有了答案。`&&`操作符首先被求值,而`||`操作符第二被求值。
但這不是因為從左到右的處理順序嗎?讓我們把操作符的順序倒過來:
```source-js
true || false && false; // true
(true || false) && false; // false -- 不
true || (false && false); // true -- 這才是勝利者!
```
現在我們證明了`&&`首先被求值,然后才是`||`,而且在這個例子中的順序實際上是與一般希望的從左到右的順序相反的。
那么什么導致了這種行為?操作符優先級。
每種語言都定義了自己的操作符優先級列表。雖然令人焦慮,但是JS開發者讀過JS的列表卻不太常見。
如果你熟知它,上面的例子一點兒都不會絆到你,因為你已經知道了`&&`要比`||`優先級高。但是我打賭有相當一部分讀者不得不將它考慮一會。
注意:?不幸的是,JS語言規范沒有將它的操作符優先級羅列在一個方便,單獨的位置。你不得不通讀并理解所有的文法規則。所以我們將試著以一種更方便的格式排列出更常見和更有用的部分。要得到完整的操作符優先級列表,參見MDN網站的“操作符優先級”(*?[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)。](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)%E3%80%82)
### 短接
在第四章中,我們在一個邊注中提到了操作符`&&`和`||`的“短接”性質。讓我們更詳細地重溫它們。
對于`&&`和`||`兩個操作符來說,如果左手邊的操作數足夠確定操作的結果,那么右手邊的操作數將?不會被求值。故而,有了“短接”(如果可能,它就會取捷徑退出)這個名字。
例如,說`a && b`,如果`a`是falsy`b`就不會被求值,因為`&&`操作數的結果已經確定了,所以再去麻煩地檢查`b`是沒有意義的。同樣的,說`a || b`,如果`a`是truthy,那么操作的結果就已經確定了,所以沒有理由再去檢查`b`。
這種短接非常有幫助,而且經常被使用:
```source-js
function doSomething(opts) {
if (opts && opts.cool) {
// ..
}
}
```
`opts && opts.cool`測試的`opts`部分就像某種保護,因為如果`opts`沒有被賦值(或不是一個`object`),那么表達式`opts.cool`就將拋出一個錯誤。`opts`測試失敗加上短接意味著`opts.cool`根本不會被求值,因此沒有錯誤!
相似地,你可以用`||`短接:
```source-js
function doSomething(opts) {
if (opts.cache || primeCache()) {
// ..
}
}
```
這里,我們首先檢查`opts.cache`,如果它存在,我們就不會調用`primeCache()`函數,如此避免了潛在的不必要的工作。
### 更緊密的綁定
讓我們把注意力轉回前面全是鏈接的操作符的復雜語句的例子,特別是`? :`三元操作符的部分。`? :`操作對的優先級與`&&`和`||`操作符比起來是高還是低?
```source-js
a && b || c ? c || b ? a : c && b : a
```
它是更像這樣:
```source-js
a && b || (c ? c || (b ? a : c) && b : a)
```
還是這樣?
```source-js
(a && b || c) ? (c || b) ? a : (c && b) : a
```
答案是第二個。但為什么?
因為`&&`優先級比`||`高,而`||`優先級比`? :`高。
所以,表達式`(a && b || c)`在`? :`參與之前被?*首先*?求值。另一種常見的解釋方式是,`&&`和`||`要比`? :`“結合的更緊密”。如果倒過來成立的話,那么`c ? c..`將結合的更緊密,那么它就會如`a && b || (c ? c..)`那樣動作(就像第一種選擇)。
### 結合性
所以,`&&`和`||`操作符首先集合,然后是`? :`操作符。但是多個同等優先級的操作符呢?它們總是從左到右或是從右到左地處理嗎?
一般來說,操作符不是左結合的就是右結合的,這要看?分組是從左邊發生還是從右邊發生。
至關重要的是,結合性與從左到右或從右到左的處理?*不是*?同一個東西。
但為什么處理是從左到右或從右到左那么重要?因為表達式可以有副作用,例如函數調用:
```source-js
var a = foo() && bar();
```
這里,`foo()`首先被求值,然后根據表達式`foo()`的結果,`bar()`可能會求值。如果`bar()`在`foo()`之前被調用絕對會得出不同的程序行為。
但是這個行為就是從左到右的處理(JavaScript中的默認行為!)—— 它與`&&`的結合性無關。在這個例子中,因為這里只有一個`&&`因此沒有相關的分組,所以根本談不上結合性。
但是像`a && b && c`這樣的表達式,分組將會隱含地發生,意味著不是`a && b`就是`b && c`會先被求值。
技術上講,`a && b && c`將會作為`(a && b) && c`處理,因為`&&`是左結合的(順帶一提,`||`也是)。然而,右結合的`a && (b && c)`也表現出相同的行為。對于相同的值,相同的表達式是按照相同的順序求值的。
注意:?如果假設`&&`是右結合的,它就會與你手動使用`( )`建立`a && (b && c)`這樣的分組的處理方式一樣。但是這仍然?不意味著?`c`將會在`b`之前被處理。右結合性的意思?不是?從右到左求值,它的意思是從右到左?分組。不管哪種方式,無論分組/結合性怎樣,嚴格的求值順序將是`a`,然后`b`,然后`c`(也就是從左到右)。
因此,除了使我們對它們定義的討論更準確以外,`&&`和`||`是左結合這件事沒有那么重要。
但事情不總是這樣。一些操作符根據左結合性與右結合性將會做出不同的行為。
考慮`? :`(“三元”或“條件”)操作符:
```source-js
a ? b : c ? d : e;
```
`? :`是右結合的,那么哪種分組表現了它將被處理的方式?
* `a ? b : (c ? d : e)`
* `(a ? b : c) ? d : e`
答案是`a ? b : (c ? d : e)`。不像上面的`&&`和`||`,在這里右結合性很重要,因為對于一些(不是全部!)值的組合來說`(a ? b : c) ? d : e`的行為將會不同。
一個這樣的例子是:
```source-js
true ? false : true ? true : true; // false
true ? false : (true ? true : true); // false
(true ? false : true) ? true : true; // true
```
在其他的值的組合中潛伏著更加微妙的不同,即便他們的最終結果是相同的。考慮:
```source-js
true ? false : true ? true : false; // false
true ? false : (true ? true : false); // false
(true ? false : true) ? true : false; // false
```
在這個場景中,相同的最終結果暗示著分組是沒有實際意義的。然而:
```source-js
var a = true, b = false, c = true, d = true, e = false;
a ? b : (c ? d : e); // false, 僅僅對 `a` 和 `b` 求值
(a ? b : c) ? d : e; // false, 對 `a`, `b` 和 `e` 求值
```
這樣,我們就清楚地證明了`? :`是右結合的,而且在這個操作符與它自己鏈接的方式上,右結合性是發揮影響的。
另一個右結合(分組)的例子是`=`操作符。回想本章早先的鏈式賦值的例子:
```source-js
var a, b, c;
a = b = c = 42;
```
我們早先斷言過,`a = b = c = 42`的處理方式是,首先對`c = 42`賦值求值,然后是`b = ..`,最后是`a = ..`。為什么?因為右結合性,它實際上這樣看待這個語句:`a = (b = (c = 42))`。
記得本章前面,我們的復雜賦值表達式的實例嗎?
```source-js
var a = 42;
var b = "foo";
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // 42
```
隨著我們使用優先級和結合性的知識把自己武裝起來,我們應當可以像這樣把這段代碼分解為它的分組行為:
```source-js
((a && b) || c) ? ((c || b) ? a : (c && b)) : a
```
或者,如果這樣容易理解的話,可以用縮進表達:
```source-js
(
(a && b)
||
c
)
?
(
(c || b)
?
a
:
(c && b)
)
:
a
```
讓我們解析它:
1. `(a && b)`是`"foo"`.
2. `"foo" || c`是`"foo"`.
3. 對于第一個`?`測試,`"foo"`是truthy。
4. `(c || b)`是`"foo"`.
5. 對于第二個`?`測試,?`"foo"`是truthy。
6. `a`是`42`.
就是這樣,我們搞定了!答案是`42`,正如我們早先看到的。其實它沒那么難,不是嗎?
### 消除歧義
現在你應該對操作符優先級(和結合性)有了更好的把握,并對理解多個鏈接的操作符如何動作感到更適應了。
但還存在一個重要的問題:我們應當一直編寫完美地依賴于操作符優先級/結合性的代碼嗎?我們應該僅在有必要強制一種不同的處理順序時使用`( )`手動分組嗎?
或者,另一方面,我們應當這樣認識嗎:雖然這樣的規則?*實際上*?是可以學懂的,但是太多的坑讓我們不得不忽略自動優先級/結合性?如果是這樣,我們應當總是使用`( )`手動分組并移除對這些自動行為的所有依賴嗎?
這種爭論是非常主觀的,而且和第四章中關于?*隱含*?強制轉換的爭論是強烈對稱的。大多數開發者對這兩個爭論的感覺是一樣的:要么他們同時接受這兩種行為并使用它們編碼,要么他們同時摒棄兩種行為并堅持手動/明確的寫法。
當然,在這個問題上,我們不能給出比我在第四章中給出的更絕對的答案。但我向你展示了利弊,并且希望促進了你更深刻的理解,以使你可以做出合理而不是人云亦云的決定。
在我看來,這里有一個重要的中間立場。我們應當將操作符優先級/結合性?*與*?`( )`手動分組兩者混合進我們的程序 —— 我在第四章中對于?*隱含的*?強制轉換的健康/安全用法做過同樣的辯論,但當然不會沒有界限地僅僅擁護它。
例如,對我來說`if (a && b && c) ..`是完全沒問題的,而我不會為了明確表現結合性而寫出`if ((a && b) && c) ..`,因為我認為這過于繁冗了。
另一方面,如果我需要鏈接兩個`? :`條件操作符,我會理所當然地使用`( )`手動分組來使我意圖的邏輯表達的絕對清晰。
因此,我在這里的意見和在第四章中的相似:在操作符優先級/結合性可以使代碼更短更干凈的地方使用操作符優先級/結合性,在`( )`手動分組可以幫你創建更清晰的代碼并減少困惑的地方使用`( )`手動分組
## 自動分號
當JavaScript認為在你的JS程序中特定的地方有一個`;`時,就算你沒在那里放一個`;`,它就會進行ASI(Automatic Semicolon Insertion —— 自動分號插入)。
為什么它這么做?因為就算你只省略了一個必需的`;`,你的程序就會失敗。不是非常寬容。ASI允許JS容忍那些通常被認為是不需要`;`的特定地方省略`;`。
必須注意的是,ASI將僅在換行存在時起作用。分號不會被插入一行的中間。
基本上,如果JS解析器在解析一行時發生了解析錯誤(缺少一個應有的`;`),而且它可以合理的插入一個`;`,它就會這么做。什么樣的地方對插入是合理的?僅在一個語句和這一行的換行之間除了空格和/或注釋沒有別的東西時。
考慮如下代碼:
```source-js
var a = 42, b
c;
```
JS應當將下一行的`c`作為`var`語句的一部分看待嗎?如果在`b`和`c`之間的任意一個地方出現一個`,`,它當然會的。但是因為沒有,所以JS認為在`b`后面有一個隱含的`;`(在換行處)。如此`c;`就剩下來作為一個獨立的表達式語句。
類似地:
```source-js
var a = 42, b = "foo";
a
b // "foo"
```
這仍然是一個沒有錯誤的合法程序,因為表達式語句也接受ASI。
有一些特定的地方ASI很有幫助,例如:
```source-js
var a = 42;
do {
// ..
} while (a) // <-- 這里需要;!
a;
```
文法要求`do..while`循環后面要有一個`;`,但是`while`或`for`循環后面則沒有。但是大多數開發者都不記得它!所以ASI幫助性地介入并插入一個。
如我們在本章早先說過的,語句塊兒不需要`;`終結,所以ASI是不必要的:
```source-js
var a = 42;
while (a) {
// ..
} // <-- 這里不需要;
a;
```
另一個ASI介入的主要情況是,與`break`,`continue`,`return`,和(ES6)`yield`關鍵字:
```source-js
function foo(a) {
if (!a) return
a *= 2;
// ..
}
```
這個`return`語句的作用不會超過換行到`a *= 2`表達式,因為ASI認為`;`終結了`return`語句。當然,`return`語句?*可以*?很容易地跨越多行,只要`return`后面不是除了換行外什么都沒有就行。
```source-js
function foo(a) {
return (
a * 2 + 3 / 12
);
}
```
同樣的道理也適用于`break`,`continue`,和`yield`。
### 糾錯
在JS社區中斗得最火熱的?*宗教戰爭*?之一(除了制表與空格以外),就是是否應當嚴重/唯一地依賴ASI。
大多數,但不是全部的,分號是可選的,但是`for ( .. ) ..`循環的頭部的兩個`;`是必須的。
在這場爭論的正方,許多開發者相信ASI是一種有用的機制,允許他們通過省略除了必須(很少幾個)以外的所有`;`寫出更簡潔(和更“美觀”)的代碼。他們經常斷言因為ASI使許多`;`成為可選的,所以一個?*不帶它們*?而正確編寫的程序,與?*帶著它們*?而正確編寫的程序沒有區別。
在這場爭論的反方,許多開發者將斷言有?*太多*?的地方可以成為意想不到的坑了,特別是對那些新來的,缺乏經驗的開發者來說,無意間被魔法般插入的`;`改變了程序的含義。類似地,一些開發者將會爭論如果他們省略了一個分號,這就是一個直白的錯誤,而且他們希望他們的工具(linter等等)在JS引擎背地里?*糾正*?它之前就抓住他。
讓我分享一下我的觀點。仔細閱讀語言規范,會發現它暗示ASI是一個?*糾錯*?過程。你可能會問,什么樣的錯誤?明確地講,是一個?解析器錯誤。換句話說,為了使解析器失敗的少一些,ASI讓它更寬容。
但是寬容什么?在我看來,一個?解析器錯誤?發生的唯一方式是,它被給予了一個不正確/錯誤的程序去解析。所以雖然ASI在嚴格地糾正解析器錯誤,但是它得到這樣的錯誤的唯一方式是,程序首先就寫錯了 —— 在文法要求使用分號的地方忽略了它們。
所以,更直率地講,當我聽到有人聲稱他們想要省略“可選的分號”時,我的大腦就將它翻譯為“我想盡量編寫最能破壞解析器但依然可以工作的程序。”
我發現這種立場很荒唐,而且省幾下鍵盤敲擊和更“美觀的代碼”的觀點是軟弱無力的。
進一步講,我不同意這和空格與制表符的爭論是同一種東西 —— 那純粹是表面上的 —— 我寧愿相信這是一個根本問題:是編寫遵循文法要求的代碼,還是編寫依賴于文法異常但僅僅將之忽略不計的代碼。
另一種看待這個問題的方式是,依賴ASI實質上將換行視為有意義的“空格”。像Python那樣的其他語言中有真正的有意義的空格。但是就今天的JavaScript來說,認為它擁有有意義的換行真的合適嗎?
我的意見是:在你知道分號是“必需的”地方使用分號,并且把你對ASI的臆測限制到最小。
不要光聽我的一面之詞。回到2012年,JavaScript的創造者Brendan Eich說過下面的話([http://brendaneich.com/2012/04/the-infernal-semicolon/):](http://brendaneich.com/2012/04/the-infernal-semicolon/)%EF%BC%9A)
> 這個故事的精神是:ASI是一種(正式地說)語法錯誤糾正過程。如果你在好像有一種普遍的有意義的換行的規則的前提下開始編碼,你將會陷入麻煩。 .. 如果回到1995年五月的那十天,我希望我使換行在JS中更有意義。 .. 如果ASI好像給了JS有意義的換行,那么要小心不要使用它。
## 錯誤
JavaScript不僅擁有不同的錯誤?*子類型*(`TypeError`,`ReferenceError`,`SyntaxError`等等),而且和其他在運行時期間發生的錯誤相比,它的文法還定義了在編譯時被強制執行的特定錯誤。
尤其是,早就有許多明確的情況應當被作為“早期錯誤”(編譯期間)被捕獲和報告。任何直接的語法錯誤都是一個早期錯誤(例如,`a = ,`),而且文法還定義了一些語法上合法但是無論怎樣都不允許的東西。
因為你的代碼還沒有開始執行,這些錯誤不能使用`try..catch`捕獲;它們只是會在你的程序進行解析/編譯時導致失敗。
提示:?在語言規范中沒有要求瀏覽器(和開發者工具)到底應當怎樣報告錯誤。所以在下面的錯誤例子中,對于哪一種錯誤的子類型會被報告或它包含什么樣的錯誤消息,你可能會在各種瀏覽器中看到不同的形式,
一個簡單的例子是正則表達式字面量中的語法。這里的JS語法沒有錯誤,而是不合法的正則表達式將會拋出一個早期錯誤:
```source-js
var a = /+foo/; // 錯誤!
```
一個賦值的目標必須是一個標識符(或者一個產生一個或多個標識符的ES6解構表達式),所以一個像`42`這樣的值在這個位置上是不合法的,因此可以立即被報告:
```source-js
var a;
42 = a; // 錯誤!
```
ES5的`strict`模式定義了更多的早期錯誤。例如,在`strict`模式中,函數參數的名稱不能重復:
```source-js
function foo(a,b,a) { } // 還好
function bar(a,b,a) { "use strict"; } // 錯誤!
```
另一種`strict`模式的早期錯誤是,一個對象字面量擁有一個以上的同名屬性:
```source-js
(function(){
"use strict";
var a = {
b: 42,
b: 43
}; // 錯誤!
})();
```
注意:?從語義上講,這樣的錯誤技術上不是?*語法*?錯誤,而是?*文法*?錯誤 —— 上面的代碼段是語法上合法的。但是因為沒有`GrammarError`類型,一些瀏覽器使用`SyntaxError`代替。
### 過早使用變量
ES6定義了一個(坦白地說,讓人困惑地命名的)新的概念,稱為TDZ(“Temporal Dead Zone” —— 時間死區)
TDZ指的是代碼中還不能使用變量引用的地方,因為它還沒有到完成它所必須的初始化。
對此最明白的例子就是ES6的`let`塊兒作用域:
```source-js
{
a = 2; // ReferenceError!
let a;
}
```
賦值`a = 2`在變量`a`(它確實是在`{ .. }`塊兒作用域中)被聲明`let a`初始化之前就訪問它,所以`a`位于TDZ中并拋出一個錯誤。
有趣的是,雖然`typeof`有一個例外,它對于未聲明的變量是安全的(見第一章),但是對于TDZ引用卻沒有這樣的安全例外:
```source-js
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
```
## [](https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/types%20%26%20grammar/ch5.md#%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0%E5%80%BC)函數參數值
另一個違反TDZ的例子可以在ES6的參數默認值(參見本系列的?*ES6與未來*)中看到:
```source-js
var b = 3;
function foo( a = 42, b = a + b + 5 ) {
// ..
}
```
在賦值中的`b`引用將在參數`b`的TDZ中發生(不會被拉到外面的`b`引用),所以它會拋出一個錯誤。然而,賦值中的`a`是沒有問題的,因為那時參數`a`的TDZ已經過去了。
當使用ES6的參數默認值時,如果你省略一個參數,或者你在它的位置上傳遞一個`undefined`值的話,就會應用這個默認值。
```source-js
function foo( a = 42, b = a + 1 ) {
console.log( a, b );
}
foo(); // 42 43
foo( undefined ); // 42 43
foo( 5 ); // 5 6
foo( void 0, 7 ); // 42 7
foo( null ); // null 1
```
注意:?在表達式`a + 1`中`null`被強制轉換為值`0`。更多信息參考第四章。
從ES6參數默認值的角度看,忽略一個參數和傳遞一個`undefined`值之間沒有區別。然而,有一個辦法可以在一些情況下探測到這種區別:
```source-js
function foo( a = 42, b = a + 1 ) {
console.log(
arguments.length, a, b,
arguments[0], arguments[1]
);
}
foo(); // 0 42 43 undefined undefined
foo( 10 ); // 1 10 11 10 undefined
foo( 10, undefined ); // 2 10 11 10 undefined
foo( 10, null ); // 2 10 null 10 null
```
即便參數默認值被應用到了參數`a`和`b`上,但是如果沒有參數傳入這些值槽,數組`arguments`也不會有任何元素。
反過來,如果你明確地傳入一個`undefined`參數,在數組`argument`中就會為這個參數存在一個元素,但它將是`undefined`,并且與同一值槽中的被命名參數將被提供的默認值不同。
雖然ES6參數默認值會在數組`arguments`的值槽和相應的命名參數變量之間造成差異,但是這種脫節也會以詭異的方式發生在ES5中:
```source-js
function foo(a) {
a = 42;
console.log( arguments[0] );
}
foo( 2 ); // 42 (鏈接了)
foo(); // undefined (沒鏈接)
```
如果你傳遞一個參數,`arguments`的值槽和命名的參數總是鏈接到同一個值上。如果你省略這個參數,就沒有這樣的鏈接會發生。
但是在`strict`模式下,這種鏈接無論怎樣都不存在了:
```source-js
function foo(a) {
"use strict";
a = 42;
console.log( arguments[0] );
}
foo( 2 ); // 2 (沒鏈接)
foo(); // undefined (沒鏈接)
```
依賴于這樣的鏈接幾乎可以肯定是一個壞主意,而且事實上這種連接本身是一種抽象泄漏,它暴露了引擎的底層實現細節,而不是一個合適的設計特性。
`arguments`數組的使用已經廢棄了(特別是被ES6`...`剩余參數取代以后 —— 參見本系列的?*ES6與未來*),但這不意味著它都是不好的。
在ES6以前,要得到向另一個函數傳遞的所有參數值的數組,`arguments`是唯一的辦法,它被證實十分有用。你也可以安全地混用被命名參數和`arguments`數組,只要你遵循一個簡單的規則:絕不同時引用一個被命名參數?*和*?它相應的`arguments`值槽。如果你能避開那種錯誤的實踐,你就永遠也不會暴露這種易泄漏的鏈接行為。
```source-js
function foo(a) {
console.log( a + arguments[1] ); // 安全!
}
foo( 10, 32 ); // 42
```
## `try..finally`
你可能很熟悉`try..catch`塊兒是如何工作的。但是你有沒有停下來考慮過可以與之成對出現的`finally`子句呢?事實上,你有沒有意識到`try`只要求`catch`和`finally`兩者之一,雖然如果有需要它們可以同時出現。
在`finally`子句中的代碼?*總是*?運行的(無論發生什么),而且它總是在`try`(和`catch`,如果存在的話)完成后立即運行,在其他任何代碼之前。從一種意義上說,你似乎可以認為`finally`子句中的代碼是一個回調函數,無論塊兒中的其他代碼如何動作,它總是被調用。
那么如果在`try`子句內部有一個`return`語句將會怎樣?很明顯它將返回一個值,對吧?但是調用端代碼是在`finally`之前還是之后才收到這個值呢?
```source-js
function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42
```
`return 42`立即運行,它設置好`foo()`調用的完成值。這個動作完成了`try`子句而`finally`子句接下來立即運行。只有這之后`foo()`函數才算完成,所以被返回的完成值交給`console.log(..)`語句使用。
對于`try`內部的`throw`來說,行為是完全相同的:
```source-js
function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// Uncaught Exception: 42
```
現在,如果一個異常從`finally`子句中被拋出(偶然地或有意地),它將會作為這個函數的主要完成值進行覆蓋。如果`try`塊兒中的前一個`return`已經設置好了這個函數的完成值,那么這個值就會被拋棄。
```source-js
function foo() {
try {
return 42;
}
finally {
throw "Oops!";
}
console.log( "never runs" );
}
console.log( foo() );
// Uncaught Exception: Oops!
```
其他的諸如`continue`和`break`這樣的非線性控制語句表現出與`return`和`throw`相似的行為是沒什么令人吃驚的:
```source-js
for (var i=0; i<10; i++) {
try {
continue;
}
finally {
console.log( i );
}
}
// 0 1 2 3 4 5 6 7 8 9
```
`console.log(i)`語句在`continue`語句引起的每次循環迭代的末尾運行。然而,它依然是運行在更新語句`i++`之前的,這就是為什么打印出的值是`0..9`而非`1..10`。
注意:?ES6在generator(參見本系列的?*異步與性能*)中增加了`yield`語句,generator從某些方面可以看作是中間的`return`語句。然而,和`return`不同的是,一個`yield`在generator被推進前不會完成,這意味著`try { .. yield .. }`還沒有完成。所以附著在其上的`finally`子句將不會像它和`return`一起時那樣,在`yield`之后立即運行。
一個在`finally`內部的`return`有著覆蓋前一個`try`或`catch`子句中的`return`的特殊能力,但是僅在`return`被明確調用的情況下:
```source-js
function foo() {
try {
return 42;
}
finally {
// 這里沒有 `return ..`,所以返回值不會被覆蓋
}
}
function bar() {
try {
return 42;
}
finally {
// 覆蓋前面的 `return 42`
return;
}
}
function baz() {
try {
return 42;
}
finally {
// 覆蓋前面的 `return 42`
return "Hello";
}
}
foo(); // 42
bar(); // undefined
baz(); // "Hello"
```
一般來說,在函數中省略`return`和`return;`或者`return undefined;`是相同的,但是在一個`finally`塊兒內部,`return`的省略不是用一個`return undefined`覆蓋;它只是讓前一個`return`繼續生效。
事實上,如果將打了標簽的`break`(在本章早先討論過)與`finally`相組合,我們真的可以制造一種瘋狂:
```source-js
function foo() {
bar: {
try {
return 42;
}
finally {
// 跳出標記為`bar`的塊兒
break bar;
}
}
console.log( "Crazy" );
return "Hello";
}
console.log( foo() );
// Crazy
// Hello
```
但是……別這么做。說真的。使用一個`finally`?+ 打了標簽的`break`實質上取消了`return`,這是你在盡最大的努力制造最令人困惑的代碼。我打賭沒有任何注釋可以拯救這段代碼。
## `switch`
讓我們簡單探索一下`switch`語句,某種`if..else if..else..`語句鏈的語法縮寫。
```source-js
switch (a) {
case 2:
// 做一些事
break;
case 42:
// 做另一些事
break;
default:
// 這里是后備操作
}
```
如你所見,它對`a`求值一次,然后將結果值與每個`case`表達式進行匹配(這里只是一些簡單的值表達式)。如果找到一個匹配,就會開始執行那個匹配的`case`,它將會持續執行直到遇到一個`break`或者遇到`switch`塊兒的末尾。
這些可能不會令你吃驚,但是關于`switch`,有幾個你以前可能從沒注意過的奇怪的地方。
首先,在表達式`a`和每一個`case`表達式之間的匹配與`===`算法(見第四章)是相同的。`switch`經常在`case`語句中使用絕對值,就像上面展示的,因此嚴格匹配是恰當的。
然而,你也許希望允許寬松等價(也就是`==`,見第四章),而這么做你需要“黑”一下`switch`語句:
```source-js
var a = "42";
switch (true) {
case a == 10:
console.log( "10 or '10'" );
break;
case a == 42:
console.log( "42 or '42'" );
break;
default:
// 永遠不會運行到這里
}
// 42 or '42'
```
這可以工作是因為`case`子句可以擁有任何表達式(不僅是簡單值),這意味著它將用這個表達式的結果與測試表達式(`true`)進行嚴格匹配。因為這里`a == 42`的結果為`true`,所以匹配成功。
盡管`==`,`switch`的匹配本身依然是嚴格的,在這里是`true`和`true`之間。如果`case`表達式得出truthy的結果而不是嚴格的`true`,它就不會工作。例如如果在你的表達式中使用`||`或`&&`這樣的“邏輯操作符”,這就可能咬到你:
```source-js
var a = "hello world";
var b = 10;
switch (true) {
case (a || b == 10):
// 永遠不會運行到這里
break;
default:
console.log( "Oops" );
}
// Oops
```
因為`(a || b == 10)`的結果是`"hello world"`而不是`true`,所以嚴格匹配失敗了。這種情況下,修改的方法是強制表達式明確成為一個`true`或`false`,比如`case !!(a || b == 10):`(見第四章)。
最后,`default`子句是可選的,而且它不一定非要位于末尾(雖然那是一種強烈的慣例)。即使是在`default`子句中,是否遇到`break`的規則也是一樣的:
```source-js
var a = 10;
switch (a) {
case 1:
case 2:
// 永遠不會運行到這里
default:
console.log( "default" );
case 3:
console.log( "3" );
break;
case 4:
console.log( "4" );
}
// default
// 3
```
注意:?就像我們前面討論的打標簽的`break`,`case`子句內部的`break`也可以被打標簽。
這段代碼的處理方式是,它首先通過所有的`case`子句,沒有找到匹配,然后它回到`default`子句開始執行。因為這里沒有`break`,它會繼續走進已經被跳過的塊兒`case 3`,在遇到那個`break`后才會停止。
雖然這種有些迂回的邏輯在JavaScript中是明顯可能的,但是它幾乎不可能制造出合理或易懂的代碼。要對你自己是否想要創建這種環狀的邏輯流程保持懷疑,如果你真的想要這么做,確保你留下了大量的代碼注釋來解釋你要做什么!
## 復習
JavaScript文法有相當多的微妙之處,我們作為開發者應當比平常多花一點兒時間來關注它。一點兒努力可以幫助你鞏固對這個語言更深層次的知識。
語句和表達式在英語中有類似的概念 —— 語句就像句子,而表達式就像短語。表達式可以是純粹的/自包含的,或者他們可以有副作用。
JavaScript文法層面的語義用法規則(也就是上下文),是在純粹的語法之上的。例如,用于你程序中不同地方的`{ }`可以意味著塊兒,`object`字面量,(ES6)解構語句,或者(ES6)被命名的函數參數。
JavaScript操作符都有嚴格定義的優先級(哪一個操作符首先結合)和結合性(多個操作符表達式如何隱含地分組)規則。一旦你學會了這些規則,你就可以自己決定優先級/結合性是否是為了它們自己有利而?*過于明確*,或者它們是否會對編寫更短,更干凈的代碼有所助益。
ASI(自動分號插入)是一種內建在JS引擎找中的解析器糾錯機制,它允許JS引擎在特定的環境下,在需要`;`但是被省略了的地方,并且插入可以糾正解析錯誤時,插入一個`;`。有一場爭論是關于這種行為是否暗示著大多數`;`都是可選的(而且為了更干凈的代碼可以/應當省略),或者是否它意味著省略它們是在制造JS引擎幫你掃清的錯誤。
JavaScript有幾種類型的錯誤,但很少有人知道它有兩種類別的錯誤:“早期”(編譯器拋出的不可捕獲的)和“運行時”(可以`try..catch`的)。所有在程序運行之前就使它停止的語法錯誤都明顯是早期錯誤,但也有一些別的錯誤。
函數參數值與它們正式聲明的命名參數之間有一種有趣的聯系。明確地說,如果你不小心,`arguments`數組會有一些泄漏抽象行為的坑。盡可能避開`arguments`,但如果你必須使用它,那就設法避免同時使用`arguments`中帶有位置的值槽,和相同參數的命名參數。
附著在`try`(或`try..catch`)上的`finall`在執行處理順序上提供了一些非常有趣的能力。這些能力中的一些可以很有幫助,但是它也可能制造許多困惑,特別是在與打了標簽的塊兒組合使用時。像往常一樣,為了更好更干凈的代碼而使用`finally`,不是為了顯得更聰明或更糊涂。
`switch`為`if..else if..`語句提供了一個不錯的縮寫形式,但是要小心許多常見的關于它的簡化假設。如果你不小心,會有幾個奇怪的地方絆倒你,但是`switch`手上也有一些隱藏的高招!