JavaScript 權威指南第七版(GPT 重譯)(二)

绝不原创的飞龙發表於2024-03-22

第四章:表示式和運算子

本章記錄了 JavaScript 表示式以及構建許多這些表示式的運算子。表示式 是 JavaScript 的短語,可以 評估 以產生一個值。在程式中直接嵌入的常量是一種非常簡單的表示式。變數名也是一個簡單表示式,它評估為分配給該變數的任何值。複雜表示式是由簡單表示式構建的。例如,一個陣列訪問表示式由一個評估為陣列的表示式、一個開放方括號、一個評估為整數的表示式和一個閉合方括號組成。這個新的、更復雜的表示式評估為儲存在指定陣列索引處的值。類似地,函式呼叫表示式由一個評估為函式物件的表示式和零個或多個額外表示式組成,這些額外表示式用作函式的引數。

從簡單表示式中構建複雜表示式的最常見方法是使用 運算子。運算子以某種方式結合其運算元的值(通常是兩個運算元中的一個)並評估為一個新值。乘法運算子 * 是一個簡單的例子。表示式 x * y 評估為表示式 xy 的值的乘積。為簡單起見,我們有時說一個運算子 返回 一個值,而不是“評估為”一個值。

本章記錄了 JavaScript 的所有運算子,並解釋了不使用運算子的表示式(如陣列索引和函式呼叫)。如果您已經瞭解使用 C 風格語法的其他程式語言,您會發現大多數 JavaScript 表示式和運算子的語法已經很熟悉了。

4.1 主要表示式

最簡單的表示式,稱為 主要表示式,是那些獨立存在的表示式——它們不包括任何更簡單的表示式。JavaScript 中的主要表示式是常量或 字面值、某些語言關鍵字和變數引用。

字面量是直接嵌入到程式中的常量值。它們看起來像這樣:

1.23         // A number literal
"hello"      // A string literal
/pattern/    // A regular expression literal

JavaScript 中關於數字字面量的語法已在 §3.2 中介紹過。字串字面量在 §3.3 中有文件記錄。正規表示式字面量語法在 §3.3.5 中介紹過,並將在 §11.3 中詳細記錄。

JavaScript 的一些保留字是主要表示式:

true       // Evalutes to the boolean true value
false      // Evaluates to the boolean false value
null       // Evaluates to the null value
this       // Evaluates to the "current" object

我們在 §3.4 和 §3.5 中學習了 truefalsenull。與其他關鍵字不同,this 不是一個常量——它在程式中的不同位置評估為不同的值。this 關鍵字用於物件導向程式設計。在方法體內,this 評估為呼叫該方法的物件。檢視 §4.5、第八章(特別是 §8.2.2)和 第九章 瞭解更多關於 this 的內容。

最後,第三種主要表示式是對變數、常量或全域性物件屬性的引用:

i             // Evaluates to the value of the variable i.
sum           // Evaluates to the value of the variable sum.
undefined     // The value of the "undefined" property of the global object

當程式中出現任何識別符號時,JavaScript 假定它是一個變數、常量或全域性物件的屬性,並查詢其值。如果不存在具有該名稱的變數,則嘗試評估不存在的變數會丟擲 ReferenceError。

4.2 物件和陣列初始化器

物件陣列初始化器 是值為新建立的物件或陣列的表示式。這些初始化器表示式有時被稱為 物件字面量陣列字面量。然而,與真正的字面量不同,它們不是主要表示式,因為它們包括一些指定屬性和元素值的子表示式。陣列初始化器具有稍微簡單的語法,我們將從這些開始。

陣列初始化器是方括號內包含的逗號分隔的表示式列表。陣列初始化器的值是一個新建立的陣列。這個新陣列的元素被初始化為逗號分隔表示式的值:

[]         // An empty array: no expressions inside brackets means no elements
[1+2,3+4]  // A 2-element array.  First element is 3, second is 7

陣列初始化器中的元素表示式本身可以是陣列初始化器,這意味著這些表示式可以建立巢狀陣列:

let matrix = [[1,2,3], [4,5,6], [7,8,9]];

陣列初始化器中的元素表示式在每次評估陣列初始化器時都會被評估。這意味著陣列初始化器表示式的值在每次評估時可能會有所不同。

可以透過簡單地在逗號之間省略值來在陣列文字中包含未定義的元素。例如,以下陣列包含五個元素,包括三個未定義的元素:

let sparseArray = [1,,,,5];

在陣列初始化器中,最後一個表示式後允許有一個逗號,並且不會建立未定義的元素。然而,對於最後一個表示式之後的索引的任何陣列訪問表示式都將必然評估為未定義。

物件初始化器表示式類似於陣列初始化器表示式,但方括號被花括號替換,每個子表示式字首都帶有屬性名和冒號:

let p = { x: 2.3, y: -1.2 };  // An object with 2 properties
let q = {};                   // An empty object with no properties
q.x = 2.3; q.y = -1.2;        // Now q has the same properties as p

在 ES6 中,物件文字具有更豐富的語法(詳細資訊請參見§6.10)。物件文字可以巢狀。例如:

let rectangle = {
    upperLeft: { x: 2, y: 2 },
    lowerRight: { x: 4, y: 5 }
};

我們將在第六章和第七章再次看到物件和陣列初始化器。

4.3 函式定義表示式

函式定義表示式 定義了一個 JavaScript 函式,這種表示式的值是新定義的函式。在某種意義上,函式定義表示式是“函式文字”的一種方式,就像物件初始化器是“物件文字”一樣。函式定義表示式通常由關鍵字function後跟一個逗號分隔的零個或多個識別符號(引數名稱)的列表(在括號中)和一個 JavaScript 程式碼塊(函式體)在花括號中組成。例如:

// This function returns the square of the value passed to it.
let square = function(x) { return x * x; };

函式定義表示式也可以包括函式的名稱。函式也可以使用函式語句而不是函式表示式來定義。在 ES6 及更高版本中,函式表示式可以使用緊湊的新“箭頭函式”語法。有關函式定義的完整詳細資訊請參見第八章。

4.4 屬性訪問表示式

屬性訪問表示式 評估為物件屬性或陣列元素的值。JavaScript 為屬性訪問定義了兩種語法:

*`expression`* . *identifier*
*expression* [ *expression* ]

屬性訪問的第一種風格是一個表示式後跟一個句點和一個識別符號。表示式指定物件,識別符號指定所需屬性的名稱。屬性訪問的第二種風格在第一個表示式(物件或陣列)後跟另一個方括號中的表示式。這第二個表示式指定所需屬性的名稱或所需陣列元素的索引。以下是一些具體示例:

let o = {x: 1, y: {z: 3}}; // An example object
let a = [o, 4, [5, 6]];    // An example array that contains the object
o.x                        // => 1: property x of expression o
o.y.z                      // => 3: property z of expression o.y
o["x"]                     // => 1: property x of object o
a[1]                       // => 4: element at index 1 of expression a
a[2]["1"]                  // => 6: element at index 1 of expression a[2]
a[0].x                     // => 1: property x of expression a[0]

使用任一型別的屬性訪問表示式時,首先評估.或``之前的表示式。如果值為nullundefined,則該表示式會丟擲 TypeError,因為這是兩個 JavaScript 值,不能具有屬性。如果物件表示式後跟一個句點和一個識別符號,則查詢該識別符號命名的屬性的值,併成為表示式的整體值。如果物件表示式後跟另一個方括號中的表示式,則評估並轉換為字串。然後,表示式的整體值是由該字串命名的屬性的值。在任一情況下,如果命名屬性不存在,則屬性訪問表示式的值為undefined

.identifier語法是兩種屬性訪問選項中更簡單的一種,但請注意,只有當要訪問的屬性具有合法識別符號名稱,並且在編寫程式時知道名稱時才能使用。如果屬性名稱包含空格或標點符號,或者是數字(對於陣列),則必須使用方括號表示法。當屬性名稱不是靜態的,而是計算結果時,也使用方括號(參見[§6.3.1 中的示例)。

物件及其屬性在第六章中有詳細介紹,陣列及其元素在第七章中有介紹。

4.4.1 條件屬性訪問

ES2020 新增了兩種新的屬性訪問表示式:

*`expression`* ?. *identifier*
*expression* ?.[ *expression* ]

在 JavaScript 中,值nullundefined是唯一沒有屬性的兩個值。在使用.[]的常規屬性訪問表示式中,如果左側的表示式評估為nullundefined,則會收到 TypeError。您可以使用?.?.[]語法來防止此類錯誤。

考慮表示式a?.b。如果anullundefined,那麼該表示式將評估為undefined,而不會嘗試訪問屬性b。如果a是其他值,則a?.b將評估為a.b的評估結果(如果a沒有名為b的屬性,則該值將再次為undefined)。

這種形式的屬性訪問表示式有時被稱為“可選鏈”,因為它也適用於像這樣的更長的“鏈式”屬性訪問表示式:

let a = { b: null };
a.b?.c.d   // => undefined

a是一個物件,因此a.b是一個有效的屬性訪問表示式。但是a.b的值是null,所以a.b.c會丟擲 TypeError。透過使用?.而不是.,我們避免了 TypeError,a.b?.c評估為undefined。這意味著(a.b?.c).d將丟擲 TypeError,因為該表示式嘗試訪問值undefined的屬性。但是——這是“可選鏈”非常重要的一部分——a.b?.c.d(不帶括號)簡單地評估為undefined,不會丟擲錯誤。這是因為使用?.的屬性訪問是“短路”的:如果?.左側的子表示式評估為nullundefined,則整個表示式立即評估為undefined,而不會進一步嘗試訪問屬性。

當然,如果a.b是一個物件,並且該物件沒有名為c的屬性,則a.b?.c.d將再次丟擲 TypeError,我們將需要使用另一種條件屬性訪問:

let a = { b: {} };
a.b?.c?.d  // => undefined

使用?.[]而不是[]也可以進行條件屬性訪問。在表示式a?.[b][c]中,如果a的值為nullundefined,則整個表示式立即評估為undefined,並且子表示式bc甚至不會被評估。如果其中任何一個表示式具有副作用,則如果a未定義,則副作用不會發生:

let a;          // Oops, we forgot to initialize this variable!
let index = 0;
try {
    a[index++]; // Throws TypeError
} catch(e) {
    index       // => 1: increment occurs before TypeError is thrown
}
a?.[index++]    // => undefined: because a is undefined
index           // => 1: not incremented because ?.[] short-circuits
a[index++]      // !TypeError: can't index undefined.

使用?.?.[]進行條件屬性訪問是 JavaScript 的最新功能之一。截至 2020 年初,這種新語法在大多數主要瀏覽器的當前或測試版本中得到支援。

4.5 呼叫表示式

呼叫表示式是 JavaScript 用於呼叫(或執行)函式或方法的語法。它以標識要呼叫的函式的函式表示式開頭。函式表示式後跟一個開括號,一個逗號分隔的零個或多個參數列達式列表,以及一個閉括號。一些示例:

f(0)            // f is the function expression; 0 is the argument expression.
Math.max(x,y,z) // Math.max is the function; x, y, and z are the arguments.
a.sort()        // a.sort is the function; there are no arguments.

當呼叫表示式被評估時,首先評估函式表示式,然後評估參數列達式以生成引數值列表。如果函式表示式的值不是函式,則會丟擲 TypeError。接下來,按順序將引數值分配給函式定義時指定的引數名,然後執行函式體。如果函式使用return語句返回一個值,則該值成為呼叫表示式的值。否則,呼叫表示式的值為undefined。有關函式呼叫的完整詳細資訊,包括當參數列達式的數量與函式定義中的引數數量不匹配時會發生什麼的解釋,請參閱第八章。

每個呼叫表示式都包括一對括號和開括號前的表示式。如果該表示式是一個屬性訪問表示式,則呼叫被稱為方法呼叫。在方法呼叫中,作為屬性訪問主題的物件或陣列在執行函式體時成為this關鍵字的值。這使得物件導向程式設計正規化成為可能,其中函式(當以這種方式使用時我們稱之為“方法”)在其所屬物件上操作。詳細資訊請參閱第九章。

4.5.1 條件呼叫

在 ES2020 中,你也可以使用?.()而不是()來呼叫函式。通常當你呼叫一個函式時,如果括號左側的表示式為nullundefined或任何其他非函式值,將丟擲 TypeError。使用新的?.()呼叫語法,如果?.左側的表示式評估為nullundefined,那麼整個呼叫表示式將評估為undefined,不會丟擲異常。

陣列物件有一個sort()方法,可以選擇性地傳遞一個函式引數,該函式定義了陣列元素的期望排序順序。在 ES2020 之前,如果你想編寫一個像sort()這樣的方法,它接受一個可選的函式引數,你通常會使用一個if語句來檢查函式引數在if體中呼叫之前是否已定義:

function square(x, log) { // The second argument is an optional function
    if (log) {            // If the optional function is passed
        log(x);           // Invoke it
    }
    return x * x;         // Return the square of the argument
}

然而,使用 ES2020 的這種條件呼叫語法,你可以簡單地使用?.()編寫函式呼叫,只有在實際有值可呼叫時才會發生呼叫:

function square(x, log) { // The second argument is an optional function
    log?.(x);             // Call the function if there is one
    return x * x;         // Return the square of the argument
}

但請注意,?.()僅檢查左側是否為nullundefined。它不驗證該值實際上是否為函式。因此,在這個例子中,如果你向square()函式傳遞兩個數字,它仍會丟擲異常。

類似於條件屬性訪問表示式(§4.4.1),帶有?.()的函式呼叫是短路的:如果?.左側的值為nullundefined,則括號內的參數列達式都不會被評估:

let f = null, x = 0;
try {
    f(x++); // Throws TypeError because f is null
} catch(e) {
    x       // => 1: x gets incremented before the exception is thrown
}
f?.(x++)    // => undefined: f is null, but no exception thrown
x           // => 1: increment is skipped because of short-circuiting

帶有?.()的條件呼叫表示式對方法和函式同樣有效。但是因為方法呼叫還涉及屬性訪問,所以值得花點時間確保你理解以下表示式之間的區別:

o.m()     // Regular property access, regular invocation
o?.m()    // Conditional property access, regular invocation
o.m?.()   // Regular property access, conditional invocation

在第一個表示式中,o必須是一個具有屬性m且該屬性的值必須是一個函式的物件。在第二個表示式中,如果onullundefined,則表示式評估為undefined。但如果o有任何其他值,則它必須具有一個值為函式的屬性m。在第三個表示式中,o不能為nullundefined。如果它沒有屬性m,或者該屬性的值為null,則整個表示式評估為undefined

使用?.()進行條件呼叫是 JavaScript 的最新功能之一。截至 2020 年初,這種新語法在大多數主要瀏覽器的當前或測試版本中得到支援。

4.6 物件建立表示式

物件建立表示式建立一個新物件,並呼叫一個函式(稱為建構函式)來初始化該物件的屬性。物件建立表示式類似於呼叫表示式,只是它們以關鍵字new為字首:

new Object()
new Point(2,3)

如果在物件建立表示式中未傳遞引數給建構函式,則可以省略空括號對:

new Object
new Date

物件建立表示式的值是新建立的物件。建構函式在第九章中有更詳細的解釋。

4.7 運算子概述

運算子用於 JavaScript 的算術表示式,比較表示式,邏輯表示式,賦值表示式等。表 4-1 總結了這些運算子,並作為一個方便的參考。

請注意,大多數運算子由標點字元表示,如+=。但是,有些運算子由關鍵字表示,如deleteinstanceof。關鍵字運算子是常規運算子,就像用標點符號表示的那些一樣;它們只是具有不太簡潔的語法。

表 4-1 按運算子優先順序進行組織。列出的運算子比最後列出的運算子具有更高的優先順序。由水平線分隔的運算子具有不同的優先順序級別。標記為 A 的列給出了運算子的結合性,可以是 L(從左到右)或 R(從右到左),列 N 指定了運算元的數量。標記為 Types 的列列出了運算元的預期型別和(在→符號之後)運算子的結果型別。表後面的子章節解釋了優先順序,結合性和運算元型別的概念。這些運算子本身在討論之後分別進行了文件化。

表 4-1. JavaScript 運算子

運算子 操作 A N 型別
++ 前置或後置遞增 R 1 lval→num
-- 前置或後置遞減 R 1 lval→num
- 取反數 R 1 num→num
+ 轉換為數字 R 1 any→num
~ 反轉位 R 1 int→int
! 反轉布林值 R 1 bool→bool
delete 刪除屬性 R 1 lval→bool
typeof 確定運算元的型別 R 1 any→str
void 返回未定義的值 R 1 any→undef
** 指數 R 2 num,num→num
*, /, % 乘法,除法,取餘 L 2 num,num→num
+, - 加法,減法 L 2 num,num→num
+ 連線字串 L 2 str,str→str
<< 左移 L 2 int,int→int
>> 右移並用符號擴充套件 L 2 int,int→int
>>> 右移並用零擴充套件 L 2 int,int→int
<, <=,>, >= 按數字順序比較 L 2 num,num→bool
<, <=,>, >= 按字母順序比較 L 2 str,str→bool
instanceof 測試物件類 L 2 obj,func→bool
in 測試屬性是否存在 L 2 any,obj→bool
== 測試非嚴格相等性 L 2 any,any→bool
!= 測試非嚴格不等式 L 2 any,any→bool
=== 測試嚴格相等性 L 2 any,any→bool
!== 測試嚴格不等式 L 2 any,any→bool
& 計算按位與 L 2 int,int→int
^ 計算按位異或 L 2 int,int→int
&#124; 計算按位或 L 2 int,int→int
&& 計算邏輯與 L 2 any,any→any
&#124;&#124; 計算邏輯或 L 2 any,any→any
?? 選擇第一個定義的運算元 L 2 any,any→any
?: 選擇第二或第三個運算元 R 3 bool,any,any→any
= 分配給變數或屬性 R 2 lval,any→any
**=, *=, /=, %=, 運算並賦值 R 2 lval,any→any
+=, -=, &=, ^=, &#124;=,
<<=, >>=, >>>=
, 丟棄第一個運算元,返回第二個 L 2 any,any→any

4.7.1 運算元的數量

運算子可以根據它們期望的運算元數量(它們的arity)進行分類。大多數 JavaScript 運算子,如 * 乘法運算子,都是將兩個表示式組合成單個更復雜表示式的二元運算子。也就是說,它們期望兩個運算元。JavaScript 還支援許多一元運算子,它們將單個表示式轉換為單個更復雜表示式。表示式 −x 中的 運算子是一個一元運算子,它對運算元 x 執行否定操作。最後,JavaScript 支援一個三元運算子,條件運算子 ?:,它將三個表示式組合成單個表示式。

4.7.2 運算元和結果型別

一些運算子適用於任何型別的值,但大多數期望它們的運算元是特定型別的,並且大多數運算子返回(或計算為)特定型別的值。表 4-1 中的型別列指定了運算子的運算元型別(箭頭前)和結果型別(箭頭後)。

JavaScript 運算子通常根據需要轉換運算元的型別(參見 §3.9)。乘法運算子 * 需要數字運算元,但表示式 "3" * "5" 是合法的,因為 JavaScript 可以將運算元轉換為數字。這個表示式的值是數字 15,而不是字串“15”,當然。還要記住,每個 JavaScript 值都是“真值”或“假值”,因此期望布林運算元的運算子將使用任何型別的運算元。

一些運算子的行為取決於與它們一起使用的運算元的型別。最值得注意的是,+ 運算子新增數字運算元,但連線字串運算元。類似地,諸如 < 的比較運算子根據運算元的型別以數字或字母順序執行比較。各個運算子的描述解釋了它們的型別依賴性,並指定它們執行的型別轉換。

注意,賦值運算子和 表 4-1 中列出的其他一些運算子期望型別為 lval 的運算元。lvalue 是一個歷史術語,意思是“一個可以合法出現在賦值表示式左側的表示式”。在 JavaScript 中,變數、物件的屬性和陣列的元素都是 lvalues。

4.7.3 運算子副作用

評估簡單表示式如 2 * 3 不會影響程式的狀態,程式執行的任何未來計算也不會受到該評估的影響。然而,一些表示式具有副作用,它們的評估可能會影響未來評估的結果。賦值運算子是最明顯的例子:如果將一個值賦給變數或屬性,那麼使用該變數或屬性的任何表示式的值都會發生變化。++-- 遞增和遞減運算子也類似,因為它們執行隱式賦值。delete 運算子也具有副作用:刪除屬性就像(但不完全相同於)將 undefined 賦給屬性。

沒有其他 JavaScript 運算子會產生副作用,但是如果函式呼叫和物件建立表示式中使用的任何運算子具有副作用,則會產生副作用。

4.7.4 運算子優先順序

表 4-1 中列出的運算子按照從高優先順序到低優先順序的順序排列,水平線將同一優先順序的運算子分組。運算子優先順序控制操作執行的順序。優先順序較高的運算子(在表的頂部附近)在優先順序較低的運算子(在表的底部附近)之前執行。

考慮以下表示式:

w = x + y*z;

乘法運算子*的優先順序高於加法運算子+,因此先執行乘法。此外,賦值運算子=的優先順序最低,因此在右側所有操作完成後執行賦值。

可以透過顯式使用括號來覆蓋運算子的優先順序。要求在上一個示例中首先執行加法,寫成:

w = (x + y)*z;

注意,屬性訪問和呼叫表示式的優先順序高於表 4-1 中列出的任何運算子。考慮以下表示式:

// my is an object with a property named functions whose value is an
// array of functions. We invoke function number x, passing it argument
// y, and then we ask for the type of the value returned.
typeof my.functionsx

儘管typeof是優先順序最高的運算子之一,但typeof操作是在屬性訪問、陣列索引和函式呼叫的結果上執行的,所有這些操作的優先順序都高於運算子。

實際上,如果您對運算子的優先順序有任何疑問,最簡單的方法是使用括號使評估順序明確。重要的規則是:乘法和除法在加法和減法之前執行。賦值的優先順序非常低,幾乎總是最後執行。

當新的運算子新增到 JavaScript 時,它們並不總是自然地適應這個優先順序方案。??運算子(§4.13.2)在表中顯示為比||&&低優先順序,但實際上,它相對於這些運算子的優先順序沒有定義,並且 ES2020 要求您在混合??||&&時明確使用括號。同樣,新的**乘冪運算子相對於一元否定運算子沒有明確定義的優先順序,當將否定與乘冪結合時,必須使用括號。

4.7.5 運算子結合性

在表 4-1 中,標記為 A 的列指定了運算子的結合性。L 值指定左到右的結合性,R 值指定右到左的結合性。運算子的結合性指定了相同優先順序操作的執行順序。左到右的結合性意味著操作從左到右執行。例如,減法運算子具有左到右的結合性,因此:

w = x - y - z;

等同於:

w = ((x - y) - z);

另一方面,以下表示式:

y = a ** b ** c;
x = ~-y;
w = x = y = z;
q = a?b:c?d:e?f:g;

等同於:

y = (a ** (b ** c));
x = ~(-y);
w = (x = (y = z));
q = a?b:(c?d:(e?f:g));

因為乘冪、一元、賦值和三元條件運算子具有從右到左的結合性。

4.7.6 評估順序

運算子的優先順序和結合性指定複雜表示式中操作的執行順序,但它們不指定子表示式的評估順序。JavaScript 總是嚴格按照從左到右的順序評估表示式。例如,在表示式w = x + y * z中,首先評估子表示式w,然後是xyz。然後將yz的值相乘,加上x的值,並將結果賦給表示式w指定的變數或屬性。新增括號可以改變乘法、加法和賦值的相對順序,但不能改變從左到右的評估順序。

評估順序只有在正在評估的任何表示式具有影響另一個表示式值的副作用時才會有所不同。如果表示式x增加了一個被表示式z使用的變數,那麼評估xz之前的事實就很重要。

4.8 算術表示式

本節涵蓋對運算元執行算術或其他數值操作的運算子。乘冪、乘法、除法和減法運算子是直接的,並且首先進行討論。加法運算子有自己的子節,因為它還可以執行字串連線,並且具有一些不尋常的型別轉換規則。一元運算子和位運算子也有自己的子節。

這些算術運算子中的大多數(除非另有說明如下)可以與 BigInt(參見 §3.2.5)運算元或常規數字一起使用,只要不混合這兩種型別。

基本算術運算子包括 **(指數運算),*(乘法),/(除法),%(取模:除法後的餘數),+(加法)和 -(減法)。正如前面所述,我們將在單獨的章節討論 + 運算子。其他五個基本運算子只是評估它們的運算元,必要時將值轉換為數字,然後計算冪、乘積、商、餘數或差。無法轉換為數字的非數字運算元將轉換為 NaN 值。如果任一運算元為(或轉換為)NaN,則操作的結果(幾乎總是)為 NaN

** 運算子的優先順序高於 */%(這些運算子的優先順序又高於 +-)。與其他運算子不同,** 從右到左工作,因此 2**2**3 等同於 2**8,而不是 4**3。表示式 -3**2 存在自然的歧義。根據一元減號和指數運算子的相對優先順序,該表示式可能表示 (-3)**2-(3**2)。不同的語言處理方式不同,而 JavaScript 簡單地使得在這種情況下省略括號成為語法錯誤,強制您編寫一個明確的表示式。** 是 JavaScript 最新的算術運算子:它是在 ES2016 版本中新增到語言中的。然而,Math.pow() 函式自最早版本的 JavaScript 就已經可用,並且執行的操作與 ** 運算子完全相同。

/ 運算子將其第一個運算元除以第二個運算元。如果您習慣於區分整數和浮點數的程式語言,當您將一個整數除以另一個整數時,您可能期望得到一個整數結果。然而,在 JavaScript 中,所有數字都是浮點數,因此所有除法操作都具有浮點結果:5/2 的結果為 2.5,而不是 2。除以零會產生正無窮大或負無窮大,而 0/0 的結果為 NaN:這兩種情況都不會引發錯誤。

% 運算子計算第一個運算元對第二個運算元的模。換句話說,它返回第一個運算元除以第二個運算元的整數除法後的餘數。結果的符號與第一個運算元的符號相同。例如,5 % 2 的結果為 1-5 % 2 的結果為 -1

儘管取模運算子通常用於整數運算元,但它也適用於浮點值。例如,6.5 % 2.1 的結果為 0.2

4.8.1 + 運算子

二元 + 運算子新增數字運算元或連線字串運算元:

1 + 2                        // => 3
"hello" + " " + "there"      // => "hello there"
"1" + "2"                    // => "12"

當兩個運算元的值都是數字,或者都是字串時,+ 運算子的作用是顯而易見的。然而,在任何其他情況下,都需要進行型別轉換,並且要執行的操作取決於所執行的轉換。+ 的轉換規則優先考慮字串連線:如果其中一個運算元是字串或可轉換為字串的物件,則另一個運算元將被轉換為字串並執行連線。只有當兩個運算元都不像字串時才執行加法。

技術上,+ 運算子的行為如下:

  • 如果其運算元值中的任一值為物件,則它將使用 §3.9.3 中描述的物件轉換為原始值演算法將其轉換為原始值。日期物件透過其 toString() 方法轉換,而所有其他物件透過 valueOf() 轉換,如果該方法返回原始值。然而,大多數物件沒有有用的 valueOf() 方法,因此它們也透過 toString() 轉換。

  • 在物件轉換為原始值之後,如果其中一個運算元是字串,則另一個運算元將被轉換為字串並執行連線。

  • 否則,兩個運算元將被轉換為數字(或 NaN),然後執行加法。

以下是一些示例:

1 + 2         // => 3: addition
"1" + "2"     // => "12": concatenation
"1" + 2       // => "12": concatenation after number-to-string
1 + {}        // => "1[object Object]": concatenation after object-to-string
true + true   // => 2: addition after boolean-to-number
2 + null      // => 2: addition after null converts to 0
2 + undefined // => NaN: addition after undefined converts to NaN

最後,重要的是要注意,當 + 運算子與字串和數字一起使用時,它可能不是結合的。也就是說,結果可能取決於操作執行的順序。

例如:

1 + 2 + " blind mice"    // => "3 blind mice"
1 + (2 + " blind mice")  // => "12 blind mice"

第一行沒有括號,+ 運算子具有從左到右的結合性,因此先將兩個數字相加,然後將它們的和與字串連線起來。在第二行中,括號改變了操作順序:數字 2 與字串連線以產生一個新字串。然後數字 1 與新字串連線以產生最終結果。

4.8.2 一元算術運算子

一元運算子修改單個運算元的值以產生一個新值。在 JavaScript 中,所有一元運算子都具有高優先順序,並且都是右結合的。本節描述的算術一元運算子(+-++--)都將其單個運算元轉換為數字(如果需要的話)。請注意,標點字元 +- 既用作一元運算子又用作二元運算子。

以下是一元算術運算子:

一元加+

一元加運算子將其運算元轉換為數字(或 NaN)並返回該轉換後的值。當與已經是數字的運算元一起使用時,它不會執行任何操作。由於 BigInt 值無法轉換為常規數字,因此不能使用此運算子。

一元減-

- 作為一元運算子使用時,它將其運算元轉換為數字(如果需要的話),然後改變結果的符號。

遞增++

++ 運算子遞增(即加 1)其單個運算元,該運算元必須是左值(變數、陣列元素或物件的屬性)。該運算子將其運算元轉換為數字,將 1 新增到該數字,並將遞增後的值重新賦給變數、元素或屬性。

++ 運算子的返回值取決於其相對於運算元的位置。當在運算元之前使用時,稱為前增量運算子,它遞增運算元並計算該運算元的遞增值。當在運算元之後使用時,稱為後增量運算子,它遞增其運算元但計算該運算元的未遞增值。考慮以下兩行程式碼之間的區別:

let i = 1, j = ++i;    // i and j are both 2
let n = 1, m = n++;    // n is 2, m is 1

注意表示式 x++ 不總是等同於 x=x+1++ 運算子永遠不會執行字串連線:它總是將其運算元轉換為數字並遞增。如果 x 是字串“1”,++x 是數字 2,但 x+1 是字串“11”。

還要注意,由於 JavaScript 的自動分號插入,您不能在後增量運算子和其前面的運算元之間插入換行符。如果這樣做,JavaScript 將把運算元視為一個獨立的完整語句,並在其前插入一個分號。

這個運算子,在其前增量和後增量形式中,最常用於遞增控制 for 迴圈的計數器(§5.4.3)。

遞減--

-- 運算子期望一個左值運算元。它將運算元的值轉換為數字,減去 1,並將減少後的值重新賦給運算元。與 ++ 運算子一樣,-- 的返回值取決於其相對於運算元的位置。當在運算元之前使用時,它減少並返回減少後的值。當在運算元之後使用時,它減少運算元但返回未減少的值。在運算元之後使用時,不允許換行符。

4.8.3 位運算子

位運算子對數字的二進位制表示中的位進行低階別操作。雖然它們不執行傳統的算術運算,但在這裡被歸類為算術運算子,因為它們對數字操作並返回一個數字值。這四個運算子對運算元的各個位執行布林代數運算,表現得好像每個運算元中的每個位都是一個布林值(1=true,0=false)。另外三個位運算子用於左移和右移位。這些運算子在 JavaScript 程式設計中並不常用,如果你不熟悉整數的二進位制表示,包括負整數的二進位制補碼錶示,那麼你可能可以跳過這一部分。

位運算子期望整數運算元,並表現得好像這些值被表示為 32 位整數而不是 64 位浮點值。這些運算子將它們的運算元轉換為數字,如果需要的話,然後透過丟棄任何小數部分和超過第 32 位的任何位來將數值值強制轉換為 32 位整數。移位運算子需要一個右側運算元,介於 0 和 31 之間。在將此運算元轉換為無符號 32 位整數後,它們會丟棄超過第 5 位的任何位,從而得到適當範圍內的數字。令人驚訝的是,當這些位運算子的運算元時,NaNInfinity-Infinity 都會轉換為 0。

所有這些位運算子除了 >>> 都可以與常規數字運算元或 BigInt(參見 §3.2.5)運算元一起使用。

位與 (&)

& 運算子對其整數引數的每個位執行布林與操作。只有在兩個運算元中相應的位都設定時,結果中才設定一個位。例如,0x1234 & 0x00FF 的計算結果為 0x0034

位或 (|)

| 運算子對其整數引數的每個位執行布林或操作。如果相應的位在一個或兩個運算元中的一個或兩個中設定,則結果中設定一個位。例如,0x1234 | 0x00FF 的計算結果為 0x12FF

位異或 (^)

^ 運算子對其整數引數的每個位執行布林異或操作。異或意味著運算元一為 true 或運算元二為 true,但不是兩者都為 true。如果在這個操作的結果中設定了一個相應的位,則表示兩個運算元中的一個(但不是兩個)中設定了一個位。例如,0xFF00 ^ 0xF0F0 的計算結果為 0x0FF0

位非 (~)

~ 運算子是一個一元運算子,出現在其單個整數運算元之前。它透過反轉運算元中的所有位來執行。由於 JavaScript 中有符號整數的表示方式,將 ~ 運算子應用於一個值等同於改變其符號並減去 1。例如,~0x0F 的計算結果為 0xFFFFFFF0,或者 −16。

左移 (<<)

<< 運算子將其第一個運算元中的所有位向左移動指定的位數,該位數應為介於 0 和 31 之間的整數。例如,在操作 a << 1 中,a 的第一位(個位)變為第二位(十位),a 的第二位變為第三位,依此類推。新的第一位使用零,第 32 位的值丟失。將一個值左移一位等同於乘以 2,將兩個位置左移等同於乘以 4,依此類推。例如,7 << 2 的計算結果為 28。

帶符號右移 (>>)

>> 運算子將其第一個運算元中的所有位向右移動指定的位數(一個介於 0 和 31 之間的整數)。向右移動的位將丟失。左側填充的位取決於原始運算元的符號位,以保留結果的符號。如果第一個運算元是正數,則結果的高位為零;如果第一個運算元是負數,則結果的高位為一。向右移動一個正值相當於除以 2(捨棄餘數),向右移動兩個位置相當於整數除以 4,依此類推。例如,7 >> 1 的結果為 3,但請注意−7 >> 1 的結果為−4。

零填充右移 (>>>)

>>> 運算子與 >> 運算子類似,只是左側移入的位始終為零,不管第一個運算元的符號如何。當您希望將有符號的 32 位值視為無符號整數時,這很有用。例如,−1 >> 4 的結果為−1,但−1 >>> 4 的結果為0x0FFFFFFF。這是 JavaScript 按位運算子中唯一不能與 BigInt 值一起使用的運算子。BigInt 不透過設定高位來表示負數,而是透過特定的二進位制補碼錶示。

4.9 關係表示式

本節描述了 JavaScript 的關係運算子。這些運算子測試兩個值之間的關係(如“相等”,“小於”或“屬性”),並根據該關係是否存在返回truefalse。關係表示式始終評估為布林值,並且該值通常用於控制程式執行在ifwhilefor語句中的流程(參見第五章)。接下來的小節記錄了相等和不等運算子,比較運算子以及 JavaScript 的另外兩個關係運算子ininstanceof

4.9.1 相等和不等運算子

===== 運算子檢查兩個值是否相同,使用兩種不同的相同定義。這兩個運算子接受任何型別的運算元,並且如果它們的運算元相同則返回true,如果它們不同則返回false=== 運算子被稱為嚴格相等運算子(有時稱為身份運算子),它使用嚴格的相同定義來檢查其兩個運算元是否“相同”。== 運算子被稱為相等運算子;它使用更寬鬆的相同定義來檢查其兩個運算元是否“相等”,允許型別轉換。

!=!== 運算子測試===== 運算子的確剛好相反。!= 不等運算子如果兩個值根據==相等則返回false,否則返回true!== 運算子如果兩個值嚴格相等則返回false,否則返回true。正如您將在§4.10 中看到的,! 運算子計算布林非操作。這使得很容易記住!=!== 代表“不等於”和“不嚴格相等於”。

如§3.8 中所述,JavaScript 物件透過引用而不是值進行比較。物件等於自身,但不等於任何其他物件。如果兩個不同的物件具有相同數量的屬性,具有相同名稱和值,則它們仍然不相等。同樣,具有相同順序的相同元素的兩個陣列也不相等。

嚴格相等

嚴格相等運算子===評估其運算元,然後按照以下方式比較兩個值,不執行任何型別轉換:

  • 如果兩個值具有不同的型別,則它們不相等。

  • 如果兩個值都是null或兩個值都是undefined,它們是相等的。

  • 如果兩個值都是布林值true或都是布林值false,它們是相等的。

  • 如果一個或兩個值是NaN,它們不相等。(這很令人驚訝,但NaN值永遠不等於任何其他值,包括它自己!要檢查值x是否為NaN,請使用x !== x或全域性的isNaN()函式。)

  • 如果兩個值都是數字且具有相同的值,則它們是相等的。如果一個值是0,另一個是-0,它們也是相等的。

  • 如果兩個值都是字串且包含完全相同的 16 位值(參見§3.3 中的側邊欄)且位置相同,則它們是相等的。如果字串在長度或內容上有所不同,則它們不相等。兩個字串可能具有相同的含義和相同的視覺外觀,但仍然使用不同的 16 位值序列進行編碼。JavaScript 不執行 Unicode 規範化,因此這樣的一對字串不被認為等於=====運算子。

  • 如果兩個值引用相同的物件、陣列或函式,則它們是相等的。如果它們引用不同的物件,則它們不相等,即使兩個物件具有相同的屬性。

帶型別轉換的相等性

相等運算子==類似於嚴格相等運算子,但它不那麼嚴格。如果兩個運算元的值不是相同型別,則它嘗試一些型別轉換並再次嘗試比較:

  • 如果兩個值具有相同的型別,請按照前面描述的嚴格相等性進行測試。如果它們嚴格相等,則它們是相等的。如果它們不嚴格相等,則它們不相等。

  • 如果兩個值的型別不同,==運算子可能仍然認為它們相等。它使用以下規則和型別轉換來檢查相等性:

    • 如果一個值是null,另一個是undefined,它們是相等的。

    • 如果一個值是數字,另一個是字串,則將字串轉換為數字,然後使用轉換後的值再次嘗試比較。

    • 如果任一值為true,則將其轉換為 1,然後再次嘗試比較。如果任一值為false,則將其轉換為 0,然後再次嘗試比較。

    • 如果一個值是物件,另一個是數字或字串,則使用§3.9.3 中描述的演算法將物件轉換為原始值,然後再次嘗試比較。物件透過其toString()方法或valueOf()方法轉換為原始值。核心 JavaScript 的內建類在執行toString()轉換之前嘗試valueOf()轉換,但 Date 類除外,它執行toString()轉換。

    • 任何其他值的組合都不相等。

作為相等性測試的一個例子,考慮比較:

"1" == true  // => true

此表示式求值為true,表示這些外觀非常不同的值實際上是相等的。布林值true首先轉換為數字 1,然後再次進行比較。接下來,字串"1"轉換為數字 1。由於現在兩個值相同,比較返回true

4.9.2 比較運算子

這些比較運算子測試它們的兩個運算元的相對順序(數字或字母):

小於 (<)

<運算子在其第一個運算元小於第二個運算元時求值為true;否則,求值為false

大於 (>)

>運算子在其第一個運算元大於第二個運算元時求值為true;否則,求值為false

小於或等於 (<=)

<=運算子在其第一個運算元小於或等於第二個運算元時求值為true;否則,求值為false

大於或等於 (>=)

>=運算子在其第一個運算元大於或等於第二個運算元時求值為true;否則,求值為false

這些比較運算子的運算元可以是任何型別。但是,比較只能在數字和字串上執行,因此不是數字或字串的運算元將被轉換。

比較和轉換如下進行:

  • 如果任一運算元評估為物件,則將該物件轉換為原始值,如§3.9.3 末尾所述;如果其valueOf()方法返回原始值,則使用該值。否則,使用其toString()方法的返回值。

  • 如果在任何必要的物件到原始值轉換後,兩個運算元都是字串,則比較這兩個字串,使用字母順序,其中“字母順序”由組成字串的 16 位 Unicode 值的數值順序定義。

  • 如果在物件到原始值轉換後,至少有一個運算元不是字串,則兩個運算元都將轉換為數字並進行數值比較。0-0被視為相等。Infinity大於除自身以外的任何數字,而-Infinity小於除自身以外的任何數字。如果任一運算元是(或轉換為)NaN,則比較運算子始終返回false。儘管算術運算子不允許 BigInt 值與常規數字混合使用,但比較運算子允許數字和 BigInt 之間的比較。

請記住,JavaScript 字串是 16 位整數值的序列,並且字串比較只是對兩個字串中的值進行數值比較。Unicode 定義的數值編碼順序可能與任何特定語言或區域設定中使用的傳統排序順序不匹配。特別注意,字串比較區分大小寫,所有大寫 ASCII 字母都“小於”所有小寫 ASCII 字母。如果您沒有預期,此規則可能導致令人困惑的結果。例如,根據<運算子,字串“Zoo”在字串“aardvark”之前。

對於更強大的字串比較演算法,請嘗試String.localeCompare()方法,該方法還考慮了特定區域設定的字母順序定義。對於不區分大小寫的比較,您可以使用String.toLowerCase()String.toUpperCase()將字串轉換為全小寫或全大寫。而且,為了使用更通用且更好本地化的字串比較工具,請使用§11.7.3 中描述的 Intl.Collator 類。

+運算子和比較運算子對數字和字串運算元的行為不同。+偏向於字串:如果任一運算元是字串,則執行連線操作。比較運算子偏向於數字,只有在兩個運算元都是字串時才執行字串比較:

1 + 2        // => 3: addition.
"1" + "2"    // => "12": concatenation.
"1" + 2      // => "12": 2 is converted to "2".
11 < 3       // => false: numeric comparison.
"11" < "3"   // => true: string comparison.
"11" < 3     // => false: numeric comparison, "11" converted to 11.
"one" < 3    // => false: numeric comparison, "one" converted to NaN.

最後,請注意<=(小於或等於)和>=(大於或等於)運算子不依賴於相等或嚴格相等運算子來確定兩個值是否“相等”。相反,小於或等於運算子簡單地定義為“不大於”,大於或等於運算子定義為“不小於”。唯一的例外是當任一運算元是(或轉換為)NaN時,此時所有四個比較運算子都返回false

4.9.3 in 運算子

in運算子期望左側運算元是一個字串、符號或可轉換為字串的值。它期望右側運算元是一個物件。如果左側值是右側物件的屬性名稱,則評估為true。例如:

let point = {x: 1, y: 1};  // Define an object
"x" in point               // => true: object has property named "x"
"z" in point               // => false: object has no "z" property.
"toString" in point        // => true: object inherits toString method

let data = [7,8,9];        // An array with elements (indices) 0, 1, and 2
"0" in data                // => true: array has an element "0"
1 in data                  // => true: numbers are converted to strings
3 in data                  // => false: no element 3

4.9.4 instanceof 運算子

instanceof運算子期望左側運算元是一個物件,右側運算元標識物件類。如果左側物件是右側類的例項,則運算子評估為true,否則評估為false。第九章解釋了在 JavaScript 中,物件類由初始化它們的建構函式定義。因此,instanceof的右側運算元應該是一個函式。以下是示例:

let d = new Date();  // Create a new object with the Date() constructor
d instanceof Date    // => true: d was created with Date()
d instanceof Object  // => true: all objects are instances of Object
d instanceof Number  // => false: d is not a Number object
let a = [1, 2, 3];   // Create an array with array literal syntax
a instanceof Array   // => true: a is an array
a instanceof Object  // => true: all arrays are objects
a instanceof RegExp  // => false: arrays are not regular expressions

注意所有物件都是Object的例項。instanceof在判斷一個物件是否是某個類的例項時會考慮“超類”。如果instanceof的左運算元不是物件,則返回false。如果右運算元不是物件類,則丟擲TypeError

要理解instanceof運算子的工作原理,您必須瞭解“原型鏈”。這是 JavaScript 的繼承機制,描述在§6.3.2 中。要評估表示式o instanceof f,JavaScript 會評估f.prototype,然後在o的原型鏈中查詢該值。如果找到,則of的例項(或f的子類),運算子返回true。如果f.prototype不是o的原型鏈中的值之一,則o不是f的例項,instanceof返回false

4.10 邏輯表示式

邏輯運算子&&||!執行布林代數,通常與關係運算子結合使用,將兩個關係表示式組合成一個更復雜的表示式。這些運算子在接下來的小節中描述。為了完全理解它們,您可能需要回顧§3.4 中介紹的“真值”和“假值”概念。

4.10.1 邏輯 AND(&&)

&&運算子可以在三個不同級別理解。在最簡單的級別上,當與布林運算元一起使用時,&&對這兩個值執行布林 AND 操作:僅當其第一個運算元和第二個運算元都為true時才返回true。如果其中一個或兩個運算元為false,則返回false

&&經常用作連線兩個關係表示式的連線詞:

x === 0 && y === 0   // true if, and only if, x and y are both 0

關係表示式始終評估為truefalse,因此在這種情況下,&&運算子本身返回truefalse。關係運算子的優先順序高於&&(和||),因此可以安全地寫出不帶括號的表示式。

但是&&不要求其運算元是布林值。回想一下,所有 JavaScript 值都是“真值”或“假值”。(有關詳細資訊,請參閱§3.4。假值包括falsenullundefined0-0NaN""。所有其他值,包括所有物件,都是真值。)&&的第二個級別可以理解為真值和假值的布林 AND 運算子。如果兩個運算元都是真值,則運算子返回真值。否則,一個或兩個運算元必須是假值,運算子返回假值。在 JavaScript 中,任何期望布林值的表示式或語句都可以使用真值或假值,因此&&並不總是返回truefalse不會造成實際問題。

請注意,此描述指出該運算子返回“真值”或“假值”,但沒有指定該值是什麼。為此,我們需要在第三個最終級別描述&&。該運算子首先評估其第一個運算元,即左側的表示式。如果左側的值為假,整個表示式的值也必須為假,因此&&只返回左側的值,甚至不評估右側的表示式。

另一方面,如果左側的值為真值,則表示式的整體值取決於右側的值。如果右側的值為真值,則整體值必須為真值,如果右側的值為假值,則整體值必須為假值。因此,當左側的值為真值時,&&運算子評估並返回右側的值:

let o = {x: 1};
let p = null;
o && o.x     // => 1: o is truthy, so return value of o.x
p && p.x     // => null: p is falsy, so return it and don't evaluate p.x

重要的是要理解 && 可能會或可能不會評估其右側運算元。在這個程式碼示例中,變數 p 被設定為 null,並且表示式 p.x 如果被評估,將導致 TypeError。但是程式碼以一種慣用的方式使用 &&,以便僅在 p 為真值時才評估 p.x,而不是 nullundefined

&& 的行為有時被稱為短路,你可能會看到故意利用這種行為有條件地執行程式碼的程式碼。例如,下面兩行 JavaScript 程式碼具有等效的效果:

if (a === b) stop();   // Invoke stop() only if a === b
(a === b) && stop();   // This does the same thing

一般來說,當你在 && 的右側寫一個具有副作用(賦值、遞增、遞減或函式呼叫)的表示式時,你必須小心。這些副作用是否發生取決於左側的值。

儘管這個運算子實際上的工作方式有些複雜,但它最常用作一個簡單的布林代數運算子,適用於真值和假值。

4.10.2 邏輯 OR (||)

|| 運算子對其兩個運算元執行布林 OR 操作。如果一個或兩個運算元為真值,則返回真值。如果兩個運算元都為假值,則返回假值。

儘管 || 運算子通常被簡單地用作布林 OR 運算子,但它和 && 運算子一樣,具有更復雜的行為。它首先評估其第一個運算元,即左側的表示式。如果這個第一個運算元的值為真值,它會短路並返回該真值,而不會評估右側的表示式。另一方面,如果第一個運算元的值為假值,則 || 評估其第二個運算元並返回該表示式的值。

&& 運算子一樣,你應該避免包含副作用的右側運算元,除非你故意想要利用右側表示式可能不會被評估的事實。

這個運算子的一個慣用用法是在一組備選項中選擇第一個真值:

// If maxWidth is truthy, use that. Otherwise, look for a value in
// the preferences object. If that is not truthy, use a hardcoded constant.
let max = maxWidth || preferences.maxWidth || 500;

請注意,如果 0 是 maxWidth 的合法值,則此程式碼將無法正常工作,因為 0 是一個假值。參見 ?? 運算子(§4.13.2)以獲取替代方案。

在 ES6 之前,這種習慣通常用於函式中為引數提供預設值:

// Copy the properties of o to p, and return p
function copy(o, p) {
    p = p || {};  // If no object passed for p, use a newly created object.
    // function body goes here
}

然而,在 ES6 及以後,這個技巧不再需要,因為預設引數值可以直接寫在函式定義中:function copy(o, p={}) { ... }

4.10.3 邏輯 NOT (!)

! 運算子是一個一元運算子;它放在單個運算元之前。它的目的是反轉其運算元的布林值。例如,如果 x 是真值,!x 評估為 false。如果 x 是假值,則 !xtrue

&&|| 運算子不同,! 運算子在反轉轉換其運算元為布林值(使用 第三章 中描述的規則)之前。這意味著 ! 總是返回 truefalse,你可以透過兩次應用這個運算子將任何值 x 轉換為其等效的布林值:!!x(參見 §3.9.2)。

作為一元運算子,! 具有高優先順序並且緊密繫結。如果你想反轉類似 p && q 的表示式的值,你需要使用括號:!(p && q)。值得注意的是,我們可以使用 JavaScript 語法表達布林代數的兩個定律:

// DeMorgan's Laws
!(p && q) === (!p || !q)  // => true: for all values of p and q
!(p || q) === (!p && !q)  // => true: for all values of p and q

4.11 賦值表示式

JavaScript 使用 = 運算子將一個值分配給一個變數或屬性。例如:

i = 0;     // Set the variable i to 0.
o.x = 1;   // Set the property x of object o to 1.

= 運算子期望其左側運算元是一個 lvalue:一個變數或物件屬性(或陣列元素)。它期望其右側運算元是任何型別的任意值。賦值表示式的值是右側運算元的值。作為副作用,= 運算子將右側的值分配給左側的變數或屬性,以便將來對變數或屬性的引用評估為該值。

雖然賦值表示式通常相當簡單,但有時您可能會看到賦值表示式的值作為更大表示式的一部分使用。例如,您可以使用以下程式碼在同一表示式中賦值和測試一個值:

(a = b) === 0

如果這樣做,請確保您清楚====運算子之間的區別!請注意,=的優先順序非常低,當賦值的值要在更大的表示式中使用時,通常需要括號。

賦值運算子具有從右到左的結合性,這意味著當表示式中出現多個賦值運算子時,它們將從右到左進行評估。因此,您可以編寫如下程式碼將單個值分配給多個變數:

i = j = k = 0;       // Initialize 3 variables to 0

4.11.1 帶運算子的賦值

除了正常的=賦值運算子外,JavaScript 還支援許多其他賦值運算子,透過將賦值與其他操作結合起來提供快捷方式。例如,+=運算子執行加法和賦值。以下表示式:

total += salesTax;

等同於這個:

total = total + salesTax;

正如您所期望的那樣,+=運算子適用於數字或字串。對於數字運算元,它執行加法和賦值;對於字串運算元,它執行連線和賦值。

類似的運算子包括-=*=&=等。表 4-2 列出了它們全部。

表 4-2. 賦值運算子

運算子 示例 等價
+= a += b a = a + b
-= a -= b a = a - b
*= a *= b a = a * b
/= a /= b a = a / b
%= a %= b a = a % b
**= a **= b a = a ** b
<<= a <<= b a = a << b
>>= a >>= b a = a >> b
>>>= a >>>= b a = a >>> b
&= a &= b a = a & b
&#124;= a &#124;= b a = a &#124; b
^= a ^= b a = a ^ b

在大多數情況下,表示式:

a op= b

其中op是一個運算子,等價於表示式:

a = a op b

在第一行中,表示式a被評估一次。在第二行中,它被評估兩次。這兩種情況只有在a包含函式呼叫或增量運算子等副作用時才會有所不同。例如,以下兩個賦值是不同的:

data[i++] *= 2;
data[i++] = data[i++] * 2;

4.12 評估表示式

與許多解釋性語言一樣,JavaScript 有解釋 JavaScript 原始碼字串並對其進行評估以生成值的能力。JavaScript 使用全域性函式eval()來實現這一點:

eval("3+2")    // => 5

動態評估原始碼字串是一種強大的語言特性,在實踐中幾乎從不需要。如果您發現自己使用eval(),您應該仔細考慮是否真的需要使用它。特別是,eval()可能存在安全漏洞,您絕不應將任何源自使用者輸入的字串傳遞給eval()。由於 JavaScript 這樣複雜的語言,沒有辦法對使用者輸入進行清理以使其安全用於eval()。由於這些安全問題,一些 Web 伺服器使用 HTTP 的“內容安全策略”頭部來禁用整個網站的eval()

接下來的小節將解釋eval()的基本用法,並解釋兩個對最佳化器影響較小的受限版本。

4.12.1 eval()

eval()期望一個引數。如果傳遞的值不是字串,則它只是返回該值。如果傳遞一個字串,則它嘗試將字串解析為 JavaScript 程式碼,如果失敗則丟擲 SyntaxError。如果成功解析字串,則評估程式碼並返回字串中最後一個表示式或語句的值,如果最後一個表示式或語句沒有值,則返回undefined。如果評估的字串引發異常,則該異常從呼叫eval()傳播出來。

eval()的關鍵之處(在這種情況下呼叫)是它使用呼叫它的程式碼的變數環境。也就是說,它查詢變數的值,並以與區域性程式碼相同的方式定義新變數和函式。如果一個函式定義了一個區域性變數x,然後呼叫eval("x"),它將獲得區域性變數的值。如果它呼叫eval("x=1"),它會改變區域性變數的值。如果函式呼叫eval("var y = 3;"),它會宣告一個新的區域性變數y。另一方面,如果被評估的字串使用letconst,則宣告的變數或常量將區域性於評估,並不會在呼叫環境中定義。

類似地,函式可以使用以下程式碼宣告一個區域性函式:

eval("function f() { return x+1; }");

如果你從頂層程式碼呼叫eval(),它當然會操作全域性變數和全域性函式。

請注意,傳遞給eval()的程式碼字串必須在語法上是合理的:你不能使用它來將程式碼片段貼上到函式中。例如,寫eval("return;")是沒有意義的,因為return只在函式內部合法,而被評估的字串使用與呼叫函式相同的變數環境並不使其成為該函式的一部分。如果你的字串作為獨立指令碼是合理的(即使是非常簡短的像x=0),那麼它是可以傳遞給eval()的。否則,eval()會丟擲 SyntaxError。

4.12.2 全域性 eval()

正是eval()改變區域性變數的能力讓 JavaScript 最佳化器感到困擾。然而,作為一種解決方法,直譯器只是對呼叫eval()的任何函式進行較少的最佳化。但是,如果一個指令碼定義了eval()的別名,然後透過另一個名稱呼叫該函式,JavaScript 規範宣告,當eval()被任何名稱呼叫時,除了“eval”之外,它應該評估字串,就像它是頂層全域性程式碼一樣。被評估的程式碼可以定義新的全域性變數或全域性函式,並且可以設定全域性變數,但不會使用或修改呼叫函式的區域性變數,因此不會干擾區域性最佳化。

“直接 eval”是使用確切的、未限定名稱“eval”呼叫eval()函式的表示式(開始感覺像是一個保留字)。直接呼叫eval()使用呼叫上下文的變數環境。任何其他呼叫——間接呼叫——使用全域性物件作為其變數環境,不能讀取、寫入或定義區域性變數或函式。(直接和間接呼叫只能使用var定義新變數。在評估的字串中使用letconst會建立僅在評估中區域性的變數和常量,不會改變呼叫或全域性環境。)

以下程式碼演示:

const geval = eval;               // Using another name does a global eval
let x = "global", y = "global";   // Two global variables
function f() {                    // This function does a local eval
    let x = "local";              // Define a local variable
    eval("x += 'changed';");      // Direct eval sets local variable
    return x;                     // Return changed local variable
}
function g() {                    // This function does a global eval
    let y = "local";              // A local variable
    geval("y += 'changed';");     // Indirect eval sets global variable
    return y;                     // Return unchanged local variable
}
console.log(f(), x); // Local variable changed: prints "localchanged global":
console.log(g(), y); // Global variable changed: prints "local globalchanged":

請注意,進行全域性 eval 的能力不僅僅是為了最佳化器的需要;實際上,這是一個非常有用的功能,允許你執行字串程式碼,就像它們是獨立的頂層指令碼一樣。正如本節開頭所述,真正需要評估程式碼字串是罕見的。但是如果你確實發現有必要,你更可能想要進行全域性 eval 而不是區域性 eval。

4.12.3 嚴格 eval()

嚴格模式(參見§5.6.3)對eval()函式的行為甚至對識別符號“eval”的使用施加了進一步的限制。當從嚴格模式程式碼中呼叫eval(),或者當要評估的程式碼字串本身以“use strict”指令開頭時,eval()會使用私有變數環境進行區域性評估。這意味著在嚴格模式下,被評估的程式碼可以查詢和設定區域性變數,但不能在區域性範圍內定義新變數或函式。

此外,嚴格模式使 eval() 更像是一個運算子,有效地將“eval”變成了一個保留字。你不能用新值覆蓋 eval() 函式。你也不能宣告一個名為“eval”的變數、函式、函式引數或 catch 塊引數。

4.13 其他運算子

JavaScript 支援許多其他雜項運算子,詳細描述在以下章節。

4.13.1 條件運算子 (?😃

條件運算子是 JavaScript 中唯一的三元運算子,有時實際上被稱為三元運算子。這個運算子有時被寫為 ?:,儘管在程式碼中看起來並不完全是這樣。因為這個運算子有三個運算元,第一個在 ? 前面,第二個在 ?: 之間,第三個在 : 後面。使用方法如下:

x > 0 ? x : -x     // The absolute value of x

條件運算子的運算元可以是任何型別。第一個運算元被評估並解釋為布林值。如果第一個運算元的值為真值,則評估第二個運算元,並返回其值。否則,如果第一個運算元為假值,則評估第三個運算元,並返回其值。第二個和第三個運算元中只有一個被評估;永遠不會同時評估兩個。

雖然可以使用 if 語句 (§5.3.1) 實現類似的結果,但 ?: 運算子通常提供了一個便捷的快捷方式。以下是一個典型的用法,檢查變數是否已定義(並具有有意義的真值),如果是,則使用它,否則提供預設值:

greeting = "hello " + (username ? username : "there");

這等同於以下 if 語句,但更簡潔:

greeting = "hello ";
if (username) {
    greeting += username;
} else {
    greeting += "there";
}

4.13.2 第一個定義的 (??)

第一個定義運算子 ?? 的值為其第一個定義的運算元:如果其左運算元不是 null 且不是 undefined,則返回該值。否則,返回右運算元的值。與 &&|| 運算子一樣,?? 是短路運算:只有在第一個運算元評估為 nullundefined 時才評估第二個運算元。如果表示式 a 沒有副作用,那麼表示式 a ?? b 等效於:

(a !== null && a !== undefined) ? a : b

當你想選擇第一個定義的運算元而不是第一個真值運算元時,??|| (§4.10.2) 的一個有用替代。雖然 || 名義上是一個邏輯 OR 運算子,但它也被習慣性地用來選擇第一個非假值運算元,例如以下程式碼:

// If maxWidth is truthy, use that. Otherwise, look for a value in
// the preferences object. If that is not truthy, use a hardcoded constant.
let max = maxWidth || preferences.maxWidth || 500;

這種習慣用法的問題在於零、空字串和 false 都是假值,在某些情況下可能是完全有效的值。在這個程式碼示例中,如果 maxWidth 是零,則該值將被忽略。但如果我們將 || 運算子改為 ??,我們最終得到一個零是有效值的表示式:

// If maxWidth is defined, use that. Otherwise, look for a value in
// the preferences object. If that is not defined, use a hardcoded constant.
let max = maxWidth ?? preferences.maxWidth ?? 500;

以下是更多示例,展示了當第一個運算元為假值時 ?? 的工作原理。如果該運算元為假值但已定義,則 ?? 返回它。只有當第一個運算元為“nullish”(即 nullundefined)時,該運算子才會評估並返回第二個運算元:

let options = { timeout: 0, title: "", verbose: false, n: null };
options.timeout ?? 1000     // => 0: as defined in the object
options.title ?? "Untitled" // => "": as defined in the object
options.verbose ?? true     // => false: as defined in the object
options.quiet ?? false      // => false: property is not defined
options.n ?? 10             // => 10: property is null

請注意,如果我們使用 || 而不是 ??,這裡的 timeouttitleverbose 表示式將具有不同的值。

?? 運算子類似於 &&|| 運算子,但它的優先順序既不高於它們,也不低於它們。如果你在一個表示式中使用它與這些運算子之一,你必須使用顯式括號來指定你想要先執行哪個操作:

(a ?? b) || c   // ?? first, then ||
a ?? (b || c)   // || first, then ??
a ?? b || c     // SyntaxError: parentheses are required

?? 運算子由 ES2020 定義,在 2020 年初,所有主要瀏覽器的當前版本或 beta 版本都新支援該運算子。這個運算子正式稱為“nullish coalescing”運算子,但我避免使用這個術語,因為這個運算子選擇其運算元之一,但在我看來並沒有以任何方式“合併”它們。

4.13.3 typeof 運算子

typeof 是一個一元運算子,放置在其單個運算元之前,該運算元可以是任何型別。它的值是一個指定運算元型別的字串。Table 4-3 指定了typeof 運算子對任何 JavaScript 值的值。

表 4-3。typeof 運算子返回的值

x typeof x
undefined "undefined"
null "object"
truefalse "boolean"
任何數字或 NaN "number"
任何 BigInt "bigint"
任何字串 "string"
任何符號 "symbol"
任何函式 "function"
任何非函式物件 "object"

您可能會在表示式中使用typeof 運算子,如下所示:

// If the value is a string, wrap it in quotes, otherwise, convert
(typeof value === "string") ? "'" + value + "'" : value.toString()

注意,如果運算元值為nulltypeof 返回“object”。如果要區分null 和物件,您必須明確測試這種特殊情況的值。

儘管 JavaScript 函式是一種物件,但typeof 運算子認為函式與其他物件有足夠大的不同,因此它們有自己的返回值。

因為對於除函式之外的所有物件和陣列值,typeof 都會評估為“object”,所以它只有在區分物件和其他原始型別時才有用。為了區分一個類的物件與另一個類的物件,您必須使用其他技術,如instanceof 運算子(參見§4.9.4)、class 屬性(參見§14.4.3)或constructor 屬性(參見§9.2.2 和§14.3)。

4.13.4 delete 運算子

delete 是一個一元運算子,試圖刪除指定為其運算元的物件屬性或陣列元素。與賦值、遞增和遞減運算子一樣,delete 通常用於其屬性刪除副作用,而不是用於其返回的值。一些例子:

let o = { x: 1, y: 2}; // Start with an object
delete o.x;            // Delete one of its properties
"x" in o               // => false: the property does not exist anymore

let a = [1,2,3];       // Start with an array
delete a[2];           // Delete the last element of the array
2 in a                 // => false: array element 2 doesn't exist anymore
a.length               // => 3: note that array length doesn't change, though

請注意,刪除的屬性或陣列元素不僅僅被設定為undefined 值。當刪除屬性時,該屬性將不再存在。嘗試讀取不存在的屬性會返回undefined,但您可以使用in 運算子(§4.9.3)測試屬性的實際存在性。刪除陣列元素會在陣列中留下一個“空洞”,並且不會更改陣列的長度。結果陣列是稀疏的(§7.3)。

delete 期望其運算元為左值。如果它不是左值,則運算子不起作用並返回true。否則,delete 會嘗試刪除指定的左值。如果成功刪除指定的左值,則delete 返回true。然而,並非所有屬性都可以被刪除:不可配置的屬性(§14.1)不受刪除的影響。

在嚴格模式下,如果其運算元是未經限定的識別符號,如變數、函式或函式引數,則delete 會引發 SyntaxError:它僅在運算元為屬性訪問表示式時起作用(§4.4)。嚴格模式還指定,如果要刪除任何不可配置的(即不可刪除的)屬性,則delete 會引發 TypeError。在嚴格模式之外,這些情況不會發生異常,delete 簡單地返回false,表示無法刪除運算元。

以下是delete 運算子的一些示例用法:

let o = {x: 1, y: 2};
delete o.x;   // Delete one of the object properties; returns true.
typeof o.x;   // Property does not exist; returns "undefined".
delete o.x;   // Delete a nonexistent property; returns true.
delete 1;     // This makes no sense, but it just returns true.
// Can't delete a variable; returns false, or SyntaxError in strict mode.
delete o;
// Undeletable property: returns false, or TypeError in strict mode.
delete Object.prototype;

我們將在§6.4 中再次看到delete 運算子。

4.13.5 await 運算子

await在 ES2017 中引入,作為使 JavaScript 中的非同步程式設計更自然的一種方式。您需要閱讀第十三章以瞭解此運算子。簡而言之,await期望一個 Promise 物件(表示非同步計算)作為其唯一運算元,並使您的程式表現得好像正在等待非同步計算完成(但實際上不會阻塞,並且不會阻止其他非同步操作同時進行)。await運算子的值是 Promise 物件的完成值。重要的是,await只在使用async關鍵字宣告的函式內部合法。再次檢視第十三章獲取完整詳情。

4.13.6 void 運算子

void是一個一元運算子,出現在其單個運算元之前,該運算元可以是任何型別。這個運算子是不尋常且很少使用的;它評估其運算元,然後丟棄值並返回undefined。由於運算元值被丟棄,只有在運算元具有副作用時使用void運算子才有意義。

void運算子如此隱晦,以至於很難想出其使用的實際示例。一個情況是當您想要定義一個什麼都不返回但也使用箭頭函式快捷語法的函式時(參見§8.1.3),其中函式體是一個被評估並返回的單個表示式。如果您僅僅為了其副作用而評估表示式,並且不想返回其值,那麼最簡單的方法是在函式體周圍使用大括號。但是,作為替代方案,在這種情況下您也可以使用void運算子:

let counter = 0;
const increment = () => void counter++;
increment()   // => undefined
counter       // => 1

4.13.7 逗號運算子(,)

逗號運算子是一個二元運算子,其運算元可以是任何型別。它評估其左運算元,評估其右運算元,然後返回右運算元的值。因此,以下行:

i=0, j=1, k=2;

評估為 2,基本上等同於:

i = 0; j = 1; k = 2;

左側表示式始終被評估,但其值被丟棄,這意味著只有在左側表示式具有副作用時才有意義使用逗號運算子。逗號運算子通常使用的唯一情況是在具有多個迴圈變數的for迴圈(§5.4.3)中:

// The first comma below is part of the syntax of the let statement
// The second comma is the comma operator: it lets us squeeze 2
// expressions (i++ and j--) into a statement (the for loop) that expects 1.
for(let i=0,j=10; i < j; i++,j--) {
    console.log(i+j);
}

4.14 總結

本章涵蓋了各種主題,並且這裡有很多參考資料,您可能希望在未來繼續學習 JavaScript 時重新閱讀。然而,需要記住的一些關鍵點是:

  • 表示式是 JavaScript 程式的短語。

  • 任何表示式都可以評估為 JavaScript 值。

  • 表示式除了產生一個值外,還可能具有副作用(如變數賦值)。

  • 簡單表示式,如文字,變數引用和屬性訪問,可以與運算子結合以產生更大的表示式。

  • JavaScript 定義了用於算術,比較,布林邏輯,賦值和位操作的運算子,以及一些其他運算子,包括三元條件運算子。

  • JavaScript + 運算子用於新增數字和連線字串。

  • 邏輯運算子&&||具有特殊的“短路”行為,有時只評估它們的一個引數。常見的 JavaScript 習語要求您瞭解這些運算子的特殊行為。

第五章:語句

第四章將表示式描述為 JavaScript 短語。按照這個類比,語句是 JavaScript 句子或命令。就像英語句子用句號終止並用句號分隔開一樣,JavaScript 語句用分號終止(§2.6)。表示式被評估以產生一個值,但語句被執行以使某事發生。

使某事發生的一種方法是評估具有副作用的表示式。具有副作用的表示式,如賦值和函式呼叫,可以獨立作為語句存在,當以這種方式使用時被稱為表示式語句。另一類語句是宣告語句,它宣告新變數並定義新函式。

JavaScript 程式只不過是一系列要執行的語句。預設情況下,JavaScript 直譯器按照它們編寫的順序一個接一個地執行這些語句。改變這種預設執行順序的另一種方法是使用 JavaScript 中的一些語句或控制結構

條件語句

諸如ifswitch這樣的語句根據表示式的值使 JavaScript 直譯器執行或跳過其他語句

迴圈

諸如whilefor這樣重複執行其他語句的語句

跳轉

諸如breakreturnthrow這樣的語句會導致直譯器跳轉到程式的另一個部分

接下來的章節描述了 JavaScript 中的各種語句並解釋了它們的語法。表 5-1 在本章末尾總結了語法。JavaScript 程式只不過是一系列語句,用分號分隔開,因此一旦熟悉了 JavaScript 的語句,就可以開始編寫 JavaScript 程式。

5.1 表示式語句

JavaScript 中最簡單的語句是具有副作用的表示式。這種語句在第四章中有所展示。賦值語句是表示式語句的一個主要類別。例如:

greeting = "Hello " + name;
i *= 3;

遞增和遞減運算子++--與賦值語句相關。它們具有改變變數值的副作用,就像執行了一個賦值一樣:

counter++;

delete 運算子的重要副作用是刪除物件屬性。因此,它幾乎總是作為語句使用,而不是作為更大表示式的一部分:

delete o.x;

函式呼叫是另一種重要的表示式語句。例如:

console.log(debugMessage);
displaySpinner(); // A hypothetical function to display a spinner in a web app.

這些函式呼叫是表示式,但它們具有影響主機環境或程式狀態的副作用,並且在這裡被用作語句。如果一個函式沒有任何副作用,那麼呼叫它就沒有意義,除非它是更大表示式或賦值語句的一部分。例如,你不會僅僅計算餘弦值然後丟棄結果:

Math.cos(x);

但你可能會計算值並將其賦給一個變數以備將來使用:

cx = Math.cos(x);

請注意,這些示例中的每行程式碼都以分號結束。

5.2 複合語句和空語句

就像逗號運算子(§4.13.7)將多個表示式組合成一個單一表示式一樣,語句塊將多個語句組合成一個複合語句。語句塊只是一系列語句被花括號包圍起來。因此,以下行作為單個語句,並可以在 JavaScript 需要單個語句的任何地方使用:

{
    x = Math.PI;
    cx = Math.cos(x);
    console.log("cos(π) = " + cx);
}

關於這個語句塊有幾點需要注意。首先,它以分號結束。塊內的原始語句以分號結束,但塊本身不以分號結束。其次,塊內的行相對於包圍它們的花括號縮排。這是可選的,但它使程式碼更易於閱讀和理解。

就像表示式經常包含子表示式一樣,許多 JavaScript 語句包含子語句。形式上,JavaScript 語法通常允許單個子語句。例如,while迴圈語法包括一個作為迴圈體的單個語句。使用語句塊,您可以在這個單個允許的子語句中放置任意數量的語句。

複合語句允許您在 JavaScript 語法期望單個語句的地方使用多個語句。空語句則相反:它允許您在期望一個語句的地方不包含任何語句。空語句如下所示:

;

當執行空語句時,JavaScript 直譯器不會採取任何操作。空語句偶爾在您想要建立一個空迴圈體的迴圈時很有用。考慮以下for迴圈(for迴圈將在§5.4.3 中介紹):

// Initialize an array a
for(let i = 0; i < a.length; a[i++] = 0) ;

在這個迴圈中,所有工作都由表示式a[i++] = 0完成,不需要迴圈體。然而,JavaScript 語法要求迴圈體作為一個語句,因此使用了一個空語句——只是一個裸分號。

請注意,在for迴圈、while迴圈或if語句的右括號後意外包含分號可能導致難以檢測的令人沮喪的錯誤。例如,以下程式碼可能不會按照作者的意圖執行:

if ((a === 0) || (b === 0));  // Oops! This line does nothing...
    o = null;                 // and this line is always executed.

當您有意使用空語句時,最好以一種清晰表明您是有意這樣做的方式對程式碼進行註釋。例如:

for(let i = 0; i < a.length; a[i++] = 0) /* empty */ ;

5.3 條件語句

條件語句根據指定表示式的值執行或跳過其他語句。這些語句是您程式碼的決策點,有時也被稱為“分支”。如果想象一個 JavaScript 直譯器沿著程式碼路徑執行,條件語句是程式碼分支成兩個或多個路徑的地方,直譯器必須選擇要遵循的路徑。

以下小節解釋了 JavaScript 的基本條件語句if/else,並介紹了更復雜的多路分支語句switch

5.3.1 if

if語句是允許 JavaScript 做出決策的基本控制語句,更準確地說,是有條件地執行語句。該語句有兩種形式。第一種是:

if (*`expression`*)
    *`statement`*

在這種形式中,expression被評估。如果結果值為真值,將執行statement。如果expression為假值,則不執行statement。(有關真值和假值的定義,請參見§3.4。)例如:

if (username == null)       // If username is null or undefined,
    username = "John Doe";  // define it

或者類似地:

// If username is null, undefined, false, 0, "", or NaN, give it a new value
if (!username) username = "John Doe";

請注意,圍繞expression的括號是if語句語法的必需部分。

JavaScript 語法要求在if關鍵字和括號表示式之後有一個語句,但您可以使用語句塊將多個語句組合成一個。因此,if語句也可能如下所示:

if (!address) {
    address = "";
    message = "Please specify a mailing address.";
}

第二種形式的if語句引入了一個else子句,當expressionfalse時執行。其語法如下:

if (*`expression`*)
    *`statement1`*
else
    *`statement2`*

該語句形式在expression為真值時執行statement1,在expression為假值時執行statement2。例如:

if (n === 1)
    console.log("You have 1 new message.");
else
    console.log(`You have ${n} new messages.`);

當您有巢狀的帶有else子句的if語句時,需要謹慎確保else子句與適當的if語句配對。考慮以下行:

i = j = 1;
k = 2;
if (i === j)
    if (j === k)
        console.log("i equals k");
else
    console.log("i doesn't equal j");    // WRONG!!

在這個例子中,內部的if語句形成了外部if語句語法允許的單個語句。不幸的是,不清楚(除了縮排給出的提示外)else與哪個if配對。而且在這個例子中,縮排是錯誤的,因為 JavaScript 直譯器實際上將前一個例子解釋為:

if (i === j) {
    if (j === k)
        console.log("i equals k");
    else
        console.log("i doesn't equal j");    // OOPS!
}

JavaScript(與大多數程式語言一樣)的規則是,預設情況下else子句是最近的if語句的一部分。為了使這個例子不那麼模稜兩可,更容易閱讀、理解、維護和除錯,您應該使用花括號:

if (i === j) {
    if (j === k) {
        console.log("i equals k");
    }
} else {  // What a difference the location of a curly brace makes!
    console.log("i doesn't equal j");
}

許多程式設計師習慣將 ifelse 語句的主體(以及其他複合語句,如 while 迴圈)放在花括號中,即使主體只包含一個語句。始終如此可以防止剛才顯示的問題,我建議你採用這種做法。在這本印刷書中,我非常重視保持示例程式碼的垂直緊湊性,並且並不總是遵循自己在這個問題上的建議。

5.3.2 else if

if/else 語句評估一個表示式並根據結果執行兩個程式碼塊中的一個。但是當你需要執行多個程式碼塊中的一個時怎麼辦?一種方法是使用 else if 語句。else if 實際上不是一個 JavaScript 語句,而只是一個經常使用的程式設計習慣,當使用重複的 if/else 語句時會出現:

if (n === 1) {
    // Execute code block #1
} else if (n === 2) {
    // Execute code block #2
} else if (n === 3) {
    // Execute code block #3
} else {
    // If all else fails, execute block #4
}

這段程式碼沒有什麼特別之處。它只是一系列 if 語句,每個後續的 if 都是前一個語句的 else 子句的一部分。使用 else if 習慣比在其語法上等效的完全巢狀形式中編寫這些語句更可取,也更易讀:

if (n === 1) {
    // Execute code block #1
}
else {
    if (n === 2) {
        // Execute code block #2
    }
    else {
        if (n === 3) {
            // Execute code block #3
        }
        else {
            // If all else fails, execute block #4
        }
    }
}

5.3.3 switch

if 語句會導致程式執行流程的分支,你可以使用 else if 習慣來執行多路分支。然而,當所有分支都依賴於相同表示式的值時,這並不是最佳解決方案。在這種情況下,多次在多個 if 語句中評估該表示式是浪費的。

switch 語句正好處理這種情況。switch 關鍵字後跟著括號中的表示式和花括號中的程式碼塊:

switch(*`expression`*) {
    *`statements`*
}

然而,switch 語句的完整語法比這更復雜。程式碼塊中的各個位置都用 case 關鍵字標記,後跟一個表示式和一個冒號。當 switch 執行時,它計算表示式的值,然後尋找一個 case 標籤,其表示式的值與之相同(相同性由 === 運算子確定)。如果找到一個匹配值的 case,它會從標記為 case 的語句開始執行程式碼塊。如果找不到具有匹配值的 case,它會尋找一個標記為 default: 的語句。如果沒有 default: 標籤,switch 語句會跳過整個程式碼塊。

switch 是一個很難解釋的語句;透過一個例子,它的操作會變得更加清晰。下面的 switch 語句等同於前一節中展示的重複的 if/else 語句:

switch(n) {
case 1:                        // Start here if n === 1
    // Execute code block #1.
    break;                     // Stop here
case 2:                        // Start here if n === 2
    // Execute code block #2.
    break;                     // Stop here
case 3:                        // Start here if n === 3
    // Execute code block #3.
    break;                     // Stop here
default:                       // If all else fails...
    // Execute code block #4.
    break;                     // Stop here
}

注意這段程式碼中每個 case 結尾使用的 break 關鍵字。break 語句會在本章後面描述,它會導致直譯器跳出(或“中斷”)switch 語句並繼續執行後面的語句。switch 語句中的 case 子句只指定所需程式碼的起始點;它們不指定任何結束點。在沒有 break 語句的情況下,switch 語句會從與其表示式值匹配的 case 標籤開始執行其程式碼塊,並繼續執行語句直到達到程式碼塊的末尾。在極少數情況下,編寫“穿透”從一個 case 標籤到下一個的程式碼是有用的,但 99% 的情況下,你應該小心地用 break 語句結束每個 case。(然而,在函式內部使用 switch 時,你可以使用 return 語句代替 break 語句。兩者都用於終止 switch 語句並防止執行穿透到下一個 case。)

這裡是 switch 語句的一個更加現實的例子;它根據值的型別將值轉換為字串:

function convert(x) {
    switch(typeof x) {
    case "number":            // Convert the number to a hexadecimal integer
        return x.toString(16);
    case "string":            // Return the string enclosed in quotes
        return '"' + x + '"';
    default:                  // Convert any other type in the usual way
        return String(x);
    }
}

請注意,在前兩個示例中,case關鍵字分別後跟數字和字串字面量。這是switch語句在實踐中最常用的方式,但請注意,ECMAScript 標準允許每個case後跟任意表示式。

switch語句首先評估跟在switch關鍵字後面的表示式,然後按照它們出現的順序評估case表示式,直到找到匹配的值。匹配的情況是使用===身份運算子確定的,而不是==相等運算子,因此表示式必須在沒有任何型別轉換的情況下匹配。

因為並非每次執行switch語句時都會評估所有case表示式,所以應避免使用包含函式呼叫或賦值等副作用的case表示式。最安全的做法是將case表示式限制為常量表示式。

如前所述,如果沒有case表示式與switch表示式匹配,switch語句將從標記為default:的語句處開始執行其主體。如果沒有default:標籤,則switch語句將完全跳過其主體。請注意,在所示示例中,default:標籤出現在switch主體的末尾,跟在所有case標籤後面。這是一個邏輯和常見的位置,但實際上它可以出現在語句主體的任何位置。

5.4 迴圈

要理解條件語句,我們可以想象 JavaScript 直譯器透過原始碼的分支路徑。迴圈語句是將該路徑彎回自身以重複程式碼部分的語句。JavaScript 有五個迴圈語句:whiledo/whileforfor/of(及其for/await變體)和for/in。以下各小節依次解釋每個迴圈語句。迴圈的一個常見用途是遍歷陣列元素。§7.6 詳細討論了這種迴圈,並涵蓋了 Array 類定義的特殊迴圈方法。

5.4.1 while

就像if語句是 JavaScript 的基本條件語句一樣,while語句是 JavaScript 的基本迴圈語句。它的語法如下:

while (*`expression`*)
    *`statement`*

要執行while語句,直譯器首先評估expression。如果表示式的值為假值,則直譯器跳過作為迴圈體的statement並繼續執行程式中的下一條語句。另一方面,如果expression為真值,則直譯器執行statement並重復,跳回迴圈的頂部並再次評估expression。另一種說法是,直譯器在expression為真值時重複執行statement。請注意,您可以使用while(true)語法建立一個無限迴圈。

通常,您不希望 JavaScript 一遍又一遍地執行完全相同的操作。在幾乎每個迴圈中,一個或多個變數會隨著迴圈的每次迭代而改變。由於變數會改變,執行statement的操作可能每次迴圈時都不同。此外,如果涉及到expression中的變化變數,那麼表示式的值可能每次迴圈時都不同。這很重要;否則,一開始為真值的表示式永遠不會改變,迴圈永遠不會結束!以下是一個列印從 0 到 9 的數字的while迴圈示例:

let count = 0;
while(count < 10) {
    console.log(count);
    count++;
}

正如你所看到的,變數count從 0 開始,並且在迴圈體執行每次後遞增。一旦迴圈執行了 10 次,表示式變為false(即變數count不再小於 10),while語句結束,直譯器可以繼續執行程式中的下一條語句。許多迴圈都有像count這樣的計數變數。變數名ijk通常用作迴圈計數器,但如果使用更具描述性的名稱可以使程式碼更易於理解。

5.4.2 do/while

do/while迴圈類似於while迴圈,不同之處在於迴圈表示式在迴圈底部測試而不是在頂部測試。這意味著迴圈體始終至少執行一次。語法是:

do
    *`statement`*
while (*`expression`*);

do/while迴圈比其while表親更少使用——實際上,很少有確定要執行至少一次迴圈的情況。以下是do/while迴圈的示例:

function printArray(a) {
    let len = a.length, i = 0;
    if (len === 0) {
        console.log("Empty Array");
    } else {
        do {
            console.log(a[i]);
        } while(++i < len);
    }
}

do/while迴圈和普通的while迴圈之間有一些語法上的差異。首先,do迴圈需要do關鍵字(標記迴圈開始)和while關鍵字(標記結束並引入迴圈條件)。此外,do迴圈必須始終以分號結尾。如果迴圈體用大括號括起來,則while迴圈不需要分號。

5.4.3 for

for語句提供了一個迴圈結構,通常比while語句更方便。for語句簡化了遵循常見模式的迴圈。大多數迴圈都有某種計數變數。該變數在迴圈開始之前初始化,並在每次迴圈迭代之前進行測試。最後,在迴圈體結束之前,計數變數會遞增或以其他方式更新,然後再次測試該變數。在這種迴圈中,初始化、測試和更新是迴圈變數的三個關鍵操作。for語句將這三個操作編碼為表示式,並將這些表示式作為迴圈語法的顯式部分:

for(*`initialize`* ; *`test`* ; *`increment`*)
    *`statement`*

initializetestincrement是三個(用分號分隔的)表示式,負責初始化、測試和遞增迴圈變數。將它們都放在迴圈的第一行中可以輕鬆理解for迴圈正在做什麼,並防止遺漏初始化或遞增迴圈變數等錯誤。

解釋for迴圈如何工作的最簡單方法是展示等效的while迴圈:²

*`initialize`*;
while(*`test`*) {
    *`statement`*
    *`increment`*;
}

換句話說,initialize表示式在迴圈開始之前只計算一次。為了有用,此表示式必須具有副作用(通常是賦值)。JavaScript 還允許initialize是一個變數宣告語句,這樣您可以同時宣告和初始化迴圈計數器。test表示式在每次迭代之前進行評估,並控制迴圈體是否執行。如果test評估為真值,則執行迴圈體的statement。最後,評估increment表示式。同樣,這必須是具有副作用的表示式才能有效。通常,它是一個賦值表示式,或者使用++--運算子。

我們可以使用以下for迴圈列印從 0 到 9 的數字。將其與前一節中顯示的等效while迴圈進行對比:

for(let count = 0; count < 10; count++) {
    console.log(count);
}

當然,迴圈可能比這個簡單示例複雜得多,有時多個變數在迴圈的每次迭代中都會發生變化。這種情況是 JavaScript 中唯一常用逗號運算子的地方;它提供了一種將多個初始化和遞增表示式組合成適合在for迴圈中使用的單個表示式的方法:

let i, j, sum = 0;
for(i = 0, j = 10 ; i < 10 ; i++, j--) {
    sum += i * j;
}

到目前為止,我們所有的迴圈示例中,迴圈變數都是數字。這是很常見的,但並非必須的。以下程式碼使用for迴圈遍歷一個連結串列資料結構並返回列表中的最後一個物件(即,第一個沒有next屬性的物件):

function tail(o) {                          // Return the tail of linked list o
    for(; o.next; o = o.next) /* empty */ ; // Traverse while o.next is truthy
    return o;
}

注意,這段程式碼沒有初始化表示式。for迴圈中的三個表示式中的任何一個都可以省略,但兩個分號是必需的。如果省略測試表示式,則迴圈將永遠重複,for(;;)就像while(true)一樣是寫無限迴圈的另一種方式。

5.4.4 for/of

ES6 定義了一種新的迴圈語句:for/of。這種新型別的迴圈使用for關鍵字,但是與常規的for迴圈完全不同。(它也與我們將在§5.4.5 中描述的舊的for/in迴圈完全不同。)

for/of迴圈適用於可迭代物件。我們將在第十二章中詳細解釋物件何時被視為可迭代,但在本章中,只需知道陣列、字串、集合和對映是可迭代的:它們代表一個序列或一組元素,您可以使用for/of迴圈進行迴圈或迭代。

例如,這裡是我們如何使用for/of迴圈遍歷一個數字陣列的元素並計算它們的總和:

let data = [1, 2, 3, 4, 5, 6, 7, 8, 9], sum = 0;
for(let element of data) {
    sum += element;
}
sum       // => 45

表面上,語法看起來像是常規的for迴圈:for關鍵字後面跟著包含有關迴圈應該執行的詳細資訊的括號。在這種情況下,括號包含一個變數宣告(或者對於已經宣告的變數,只是變數的名稱),後面跟著of關鍵字和一個求值為可迭代物件的表示式,就像這種情況下的data陣列一樣。與所有迴圈一樣,for/of迴圈的主體跟在括號後面,通常在花括號內。

在剛才顯示的程式碼中,迴圈體會針對data陣列的每個元素執行一次。在執行迴圈體之前,陣列的下一個元素會被分配給元素變數。陣列元素按順序從第一個到最後一個進行迭代。

陣列是“實時”迭代的——在迭代過程中進行的更改可能會影響迭代的結果。如果我們在迴圈體內新增data.push(sum);這行程式碼,那麼我們將建立一個無限迴圈,因為迭代永遠無法到達陣列的最後一個元素。

使用物件進行for/of迴圈

物件預設情況下不可迭代。嘗試在常規物件上使用for/of會在執行時引發 TypeError:

let o = { x: 1, y: 2, z: 3 };
for(let element of o) { // Throws TypeError because o is not iterable
    console.log(element);
}

如果要遍歷物件的屬性,可以使用for/in迴圈(在§5.4.5 中介紹),或者使用for/ofObject.keys()方法:

let o = { x: 1, y: 2, z: 3 };
let keys = "";
for(let k of Object.keys(o)) {
    keys += k;
}
keys  // => "xyz"

這是因為Object.keys()返回一個物件的屬性名稱陣列,陣列可以使用for/of進行迭代。還要注意,與上面的陣列示例不同,物件的鍵的這種迭代不是實時的——在迴圈體中對物件o進行的更改不會影響迭代。如果您不關心物件的鍵,也可以像這樣迭代它們對應的值:

let sum = 0;
for(let v of Object.values(o)) {
    sum += v;
}
sum // => 6

如果您對物件屬性的鍵和值都感興趣,可以使用for/ofObject.entries()和解構賦值:

let pairs = "";
for(let [k, v] of Object.entries(o)) {
    pairs += k + v;
}
pairs  // => "x1y2z3"

Object.entries()返回一個陣列,其中每個內部陣列表示物件的一個屬性的鍵/值對。在這個程式碼示例中,我們使用解構賦值來將這些內部陣列解包成兩個單獨的變數。

使用字串進行for/of迴圈

在 ES6 中,字串是逐個字元可迭代的:

let frequency = {};
for(let letter of "mississippi") {
    if (frequency[letter]) {
        frequency[letter]++;
    } else {
        frequency[letter] = 1;
    }
}
frequency   // => {m: 1, i: 4, s: 4, p: 2}

請注意,字串是按 Unicode 程式碼點迭代的,而不是按 UTF-16 字元。字串“I ❤ ”的.length為 5(因為兩個表情符號字元分別需要兩個 UTF-16 字元來表示)。但如果您使用for/of迭代該字串,迴圈體將執行三次,分別為每個程式碼點“I”、“❤”和“”。

使用 Set 和 Map 進行 for/of

內建的 ES6 Set 和 Map 類是可迭代的。當您使用 for/of 迭代 Set 時,迴圈體會為集合的每個元素執行一次。您可以使用以下程式碼列印文字字串中的唯一單詞:

let text = "Na na na na na na na na Batman!";
let wordSet = new Set(text.split(" "));
let unique = [];
for(let word of wordSet) {
    unique.push(word);
}
unique // => ["Na", "na", "Batman!"]

Map 是一個有趣的情況,因為 Map 物件的迭代器不會迭代 Map 鍵或 Map 值,而是鍵/值對。在每次迭代中,迭代器返回一個陣列,其第一個元素是鍵,第二個元素是相應的值。給定一個 Map m,您可以像這樣迭代並解構其鍵/值對:

let m = new Map([[1, "one"]]);
for(let [key, value] of m) {
    key    // => 1
    value  // => "one"
}

使用 for/await 進行非同步迭代

ES2018 引入了一種新型別的迭代器,稱為非同步迭代器,以及與之配套的 for/of 迴圈的變體,稱為 for/await 迴圈,可與非同步迭代器一起使用。

您需要閱讀第十二章和第十三章才能理解 for/await 迴圈,但以下是程式碼示例:

// Read chunks from an asynchronously iterable stream and print them out
async function printStream(stream) {
    for await (let chunk of stream) {
        console.log(chunk);
    }
}

5.4.5 for/in

for/in 迴圈看起來很像 for/of 迴圈,只是將 of 關鍵字更改為 in。在 of 之後,for/of 迴圈需要一個可迭代物件,而 for/in 迴圈在 in 之後可以使用任何物件。for/of 迴圈是 ES6 中的新功能,但 for/in 從 JavaScript 最初就存在(這就是為什麼它具有更自然的語法)。

for/in 語句迴圈遍歷指定物件的屬性名稱。語法如下:

for (*`variable`* in *`object`*)
    *`statement`*

variable 通常命名一個變數,但它也可以是一個變數宣告或任何適合作為賦值表示式左側的內容。object 是一個求值為物件的表示式。通常情況下,statement 是作為迴圈主體的語句或語句塊。

您可能會像這樣使用 for/in 迴圈:

for(let p in o) {      // Assign property names of o to variable p
    console.log(o[p]); // Print the value of each property
}

要執行 for/in 語句,JavaScript 直譯器首先評估 object 表示式。如果它評估為 nullundefined,直譯器將跳過迴圈並繼續執行下一條語句。直譯器現在會為物件的每個可列舉屬性執行迴圈體。然而,在每次迭代之前,直譯器會評估 variable 表示式並將屬性的名稱(一個字串值)賦給它。

請注意,在 for/in 迴圈中的 variable 可以是任意表示式,只要它評估為適合賦值左側的內容。這個表示式在每次迴圈時都會被評估,這意味著它可能每次評估的結果都不同。例如,您可以使用以下程式碼將所有物件屬性的名稱複製到陣列中:

let o = { x: 1, y: 2, z: 3 };
let a = [], i = 0;
for(a[i++] in o) /* empty */;

JavaScript 陣列只是一種特殊型別的物件,陣列索引是可以用 for/in 迴圈列舉的物件屬性。例如,以下程式碼後面加上這行程式碼,將列舉陣列索引 0、1 和 2:

for(let i in a) console.log(i);

我發現在我的程式碼中常見的錯誤來源是意外使用陣列時使用 for/in 而不是 for/of。在處理陣列時,您幾乎總是希望使用 for/of 而不是 for/in

for/in 迴圈實際上並不列舉物件的所有屬性。它不會列舉名稱為符號的屬性。對於名稱為字串的屬性,它只迴圈遍歷可列舉屬性(參見§14.1)。核心 JavaScript 定義的各種內建方法都不可列舉。例如,所有物件都有一個 toString() 方法,但 for/in 迴圈不會列舉這個 toString 屬性。除了內建方法,許多內建物件的其他屬性也是不可列舉的。預設情況下,您程式碼定義的所有屬性和方法都是可列舉的(您可以使用§14.1 中解釋的技術使它們變為不可列舉)。

可列舉的繼承屬性(參見§6.3.2)也會被for/in迴圈列舉。這意味著如果您使用for/in迴圈,並且還使用定義了所有物件都繼承的屬性的程式碼,那麼您的迴圈可能不會按您的預期方式執行。因此,許多程式設計師更喜歡使用Object.keys()for/of迴圈而不是for/in迴圈。

如果for/in迴圈的主體刪除尚未列舉的屬性,則該屬性將不會被列舉。如果迴圈的主體在物件上定義了新屬性,則這些屬性可能會被列舉,也可能不會被列舉。有關for/in列舉物件屬性的順序的更多資訊,請參見§6.6.1。

5.5 跳轉

另一類 JavaScript 語句是跳轉語句。顧名思義,這些語句會導致 JavaScript 直譯器跳轉到原始碼中的新位置。break語句使直譯器跳轉到迴圈或其他語句的末尾。continue使直譯器跳過迴圈體的其餘部分,並跳回到迴圈的頂部開始新的迭代。JavaScript 允許對語句進行命名,或標記breakcontinue可以標識目標迴圈或其他語句標籤。

return語句使直譯器從函式呼叫跳回到呼叫它的程式碼,並提供呼叫的值。throw語句是一種臨時從生成器函式返回的方式。throw語句引發異常,並設計用於與try/catch/finally語句一起工作,後者建立了一個異常處理程式碼塊。這是一種複雜的跳轉語句:當丟擲異常時,直譯器會跳轉到最近的封閉異常處理程式,該處理程式可能在同一函式中或在呼叫函式的呼叫堆疊中。

關於這些跳轉語句的詳細資訊在接下來的章節中。

5.5.1 標記語句

任何語句都可以透過在其前面加上識別符號和冒號來標記

*`identifier`*: *`statement`*

透過給語句加上標籤,您為其賦予一個名稱,以便在程式的其他地方引用它。您可以為任何語句加上標籤,儘管只有為具有主體的語句加上標籤才有用,例如迴圈和條件語句。透過給迴圈命名,您可以在迴圈體內使用breakcontinue語句來退出迴圈或直接跳轉到迴圈的頂部開始下一次迭代。breakcontinue是唯一使用語句標籤的 JavaScript 語句;它們在以下子節中介紹。這裡是一個帶有標籤的while迴圈和使用標籤的continue語句的示例。

mainloop: while(token !== null) {
    // Code omitted...
    continue mainloop;  // Jump to the next iteration of the named loop
    // More code omitted...
}

用於標記語句的識別符號可以是任何合法的 JavaScript 識別符號,不能是保留字。標籤的名稱空間與變數和函式的名稱空間不同,因此您可以將相同的識別符號用作語句標籤和變數或函式名稱。語句標籤僅在其適用的語句內部定義(當然也包括其子語句)。語句不能具有包含它的語句相同的標籤,但是隻要一個語句不巢狀在另一個語句內,兩個語句可以具有相同的標籤。標記的語句本身也可以被標記。實際上,這意味著任何語句可以具有多個標籤。

5.5.2 break

單獨使用的break語句會導致最內層的迴圈或switch語句立即退出。其語法很簡單:

break;

因為它導致迴圈或switch退出,所以這種形式的break語句只有在出現在這些語句內部時才合法。

您已經看到了switch語句中break語句的示例。在迴圈中,當不再需要完成迴圈時,通常會提前退出。當迴圈具有複雜的終止條件時,通常更容易使用break語句實現其中一些條件,而不是嘗試在單個迴圈表示式中表達所有條件。以下程式碼搜尋陣列元素以找到特定值。當它在陣列中找到所需的內容時,迴圈以正常方式終止;如果在陣列中找到所需的內容,則使用break語句終止:

for(let i = 0; i < a.length; i++) {
    if (a[i] === target) break;
}

JavaScript 還允許在break關鍵字後面跟著一個語句標籤(只是識別符號,沒有冒號):

break *`labelname`*;

break與標籤一起使用時,它會跳轉到具有指定標籤的結束語句,或終止該結束語句。如果沒有具有指定標籤的結束語句,則以這種形式使用break語句是語法錯誤。使用這種形式的break語句時,命名的語句不必是迴圈或switchbreak可以“跳出”任何包含語句。這個語句甚至可以是一個僅用於使用標籤命名塊的大括號組成的語句塊。

break關鍵字和labelname之間不允許換行。這是由於 JavaScript 自動插入省略的分號:如果在break關鍵字和後面的標籤之間放置換行符,JavaScript 會認為您想使用簡單的、無標籤的語句形式,並將換行符視為分號。(參見§2.6。)

當您想要跳出不是最近的迴圈或switch的語句時,您需要帶標籤的break語句。以下程式碼演示了:

let matrix = getData();  // Get a 2D array of numbers from somewhere
// Now sum all the numbers in the matrix.
let sum = 0, success = false;
// Start with a labeled statement that we can break out of if errors occur
computeSum: if (matrix) {
    for(let x = 0; x < matrix.length; x++) {
        let row = matrix[x];
        if (!row) break computeSum;
        for(let y = 0; y < row.length; y++) {
            let cell = row[y];
            if (isNaN(cell)) break computeSum;
            sum += cell;
        }
    }
    success = true;
}
// The break statements jump here. If we arrive here with success == false
// then there was something wrong with the matrix we were given.
// Otherwise, sum contains the sum of all cells of the matrix.

最後,請注意,break語句,無論是否帶有標籤,都不能跨越函式邊界轉移控制。例如,您不能給函式定義語句加上標籤,然後在函式內部使用該標籤。

5.5.3 continue

continue語句類似於break語句。但是,continue不是退出迴圈,而是在下一次迭代時重新開始迴圈。continue語句的語法與break語句一樣簡單:

continue;

continue語句也可以與標籤一起使用:

continue *`labelname`*;

continue語句,無論是帶標籤還是不帶標籤,只能在迴圈體內使用。在其他任何地方使用它都會導致語法錯誤。

當執行continue語句時,將終止當前迴圈的迭代,並開始下一次迭代。對於不同型別的迴圈,這意味著不同的事情:

  • while迴圈中,迴圈開始時測試迴圈開頭的指定表示式,如果為true,則從頂部執行迴圈體。

  • do/while迴圈中,執行跳轉到迴圈底部,然後再次測試迴圈條件,然後重新開始迴圈。

  • for迴圈中,將評估增量表示式,並再次測試測試表示式以確定是否應進行另一次迭代。

  • for/offor/in迴圈中,迴圈將重新開始,下一個迭代值或下一個屬性名將被賦給指定的變數。

請注意whilefor迴圈中continue語句的行為差異:while迴圈直接返回到其條件,但for迴圈首先評估其增量表示式,然後返回到其條件。之前,我們考慮了for迴圈的行為,以等效的while迴圈來描述。然而,由於continue語句對這兩種迴圈的行為不同,因此僅使用while迴圈無法完全模擬for迴圈。

以下示例顯示了在發生錯誤時使用未標記的continue語句跳過當前迭代的其餘部分的情況:

for(let i = 0; i < data.length; i++) {
    if (!data[i]) continue;  // Can't proceed with undefined data
    total += data[i];
}

break語句類似,continue語句可以在巢狀迴圈中的標記形式中使用,當要重新啟動的迴圈不是直接包圍的迴圈時。同樣,與break語句一樣,continue語句和其labelname之間不允許換行。

5.5.4 return

請記住函式呼叫是表示式,所有表示式都有值。函式內部的return語句指定了該函式呼叫的值。下面是return語句的語法:

return *`expression`*;

return語句只能出現在函式體內部。在其他任何地方出現都會導致語法錯誤。當執行return語句時,包含它的函式將expression的值返回給呼叫者。例如:

function square(x) { return x*x; } // A function that has a return statement
square(2)                          // => 4

沒有return語句時,函式呼叫會依次執行函式體中的每個語句,直到到達函式末尾然後返回給呼叫者。在這種情況下,呼叫表示式評估為undefinedreturn語句通常出現在函式中的最後一個語句,但不一定非得是最後一個:當執行return語句時,函式返回給呼叫者,即使函式體中還有其他語句。

return語句也可以在沒有expression的情況下使用,使函式返回undefined給呼叫者。例如:

function displayObject(o) {
    // Return immediately if the argument is null or undefined.
    if (!o) return;
    // Rest of function goes here...
}

由於 JavaScript 的自動分號插入(§2.6),你不能在return關鍵字和其後的表示式之間插入換行符。

5.5.5 yield

yield語句與return語句非常相似,但僅在 ES6 生成器函式(參見§12.3)中使用,用於生成值序列中的下一個值而不實際返回:

// A generator function that yields a range of integers
function* range(from, to) {
    for(let i = from; i <= to; i++) {
        yield i;
    }
}

要理解yield,你必須理解迭代器和生成器,這將在第十二章中介紹。然而,為了完整起見,這裡包括了yield。(嚴格來說,yield是一個運算子而不是語句,如§12.4.2 中所解釋的。)

5.5.6 throw

異常是指示發生了某種異常情況或錯誤的訊號。丟擲異常是指示發生了這樣的錯誤或異常情況。捕獲異常是處理它 - 採取必要或適當的措施來從異常中恢復。在 JavaScript 中,每當發生執行時錯誤或程式明確使用throw語句丟擲異常時,都會丟擲異常。異常可以透過try/catch/finally語句捕獲,下一節將對此進行描述。

throw語句的語法如下:

throw *`expression`*;

expression可能會評估為任何型別的值。你可以丟擲一個代表錯誤程式碼的數字,或者包含人類可讀錯誤訊息的字串。當 JavaScript 直譯器本身丟擲錯誤時,會使用 Error 類及其子類,你也可以使用它們。一個 Error 物件有一個name屬性指定錯誤型別,一個message屬性儲存傳遞給建構函式的字串。下面是一個示例函式,當使用無效引數呼叫時會丟擲一個 Error 物件:

function factorial(x) {
    // If the input argument is invalid, throw an exception!
    if (x < 0) throw new Error("x must not be negative");
    // Otherwise, compute a value and return normally
    let f;
    for(f = 1; x > 1; f *= x, x--) /* empty */ ;
    return f;
}
factorial(4)   // => 24

當丟擲異常時,JavaScript 直譯器立即停止正常程式執行,並跳轉到最近的異常處理程式。異常處理程式使用try/catch/finally語句的catch子句編寫,下一節將對其進行描述。如果丟擲異常的程式碼塊沒有關聯的catch子句,直譯器將檢查下一個最高階別的封閉程式碼塊,看看它是否有與之關聯的異常處理程式。這將一直持續下去,直到找到處理程式。如果在一個不包含try/catch/finally語句來處理異常的函式中丟擲異常,異常將傳播到呼叫該函式的程式碼。透過這種方式,異常透過 JavaScript 方法的詞法結構向上傳播,並沿著呼叫堆疊向上傳播。如果從未找到異常處理程式,異常將被視為錯誤並報告給使用者。

5.5.7 try/catch/finally

try/catch/finally語句是 JavaScript 的異常處理機制。該語句的try子句簡單地定義了要處理異常的程式碼塊。try塊後面是一個catch子句,當try塊內部發生異常時,將呼叫一組語句。catch子句後面是一個finally塊,其中包含清理程式碼,無論try塊中發生了什麼,都保證會執行。catchfinally塊都是可選的,但try塊必須至少伴隨其中一個。trycatchfinally塊都以大括號開始和結束。這些大括號是語法的必要部分,即使一個子句只包含一個語句也不能省略。

以下程式碼示例說明了try/catch/finally語句的語法和目的:

try {
    // Normally, this code runs from the top of the block to the bottom
    // without problems. But it can sometimes throw an exception,
    // either directly, with a throw statement, or indirectly, by calling
    // a method that throws an exception.
}
catch(e) {
    // The statements in this block are executed if, and only if, the try
    // block throws an exception. These statements can use the local variable
    // e to refer to the Error object or other value that was thrown.
    // This block may handle the exception somehow, may ignore the
    // exception by doing nothing, or may rethrow the exception with throw.
}
finally {
    // This block contains statements that are always executed, regardless of
    // what happens in the try block. They are executed whether the try
    // block terminates:
    //   1) normally, after reaching the bottom of the block
    //   2) because of a break, continue, or return statement
    //   3) with an exception that is handled by a catch clause above
    //   4) with an uncaught exception that is still propagating
}

請注意,catch關鍵字通常後面跟著一個括號中的識別符號。這個識別符號類似於函式引數。當捕獲到異常時,與異常相關聯的值(例如一個 Error 物件)將被分配給這個引數。與catch子句關聯的識別符號具有塊作用域——它只在catch塊內定義。

這裡是try/catch語句的一個實際例子。它使用了前一節中定義的factorial()方法以及客戶端 JavaScript 方法prompt()alert()來進行輸入和輸出:

try {
    // Ask the user to enter a number
    let n = Number(prompt("Please enter a positive integer", ""));
    // Compute the factorial of the number, assuming the input is valid
    let f = factorial(n);
    // Display the result
    alert(n + "! = " + f);
}
catch(ex) {     // If the user's input was not valid, we end up here
    alert(ex);  // Tell the user what the error is
}

這個例子是一個沒有finally子句的try/catch語句。雖然finally不像catch那樣經常使用,但它也是有用的。然而,它的行為需要額外的解釋。如果try塊的任何部分被執行,finally子句將被執行。它通常用於在try子句中的程式碼執行完畢後進行清理。

在正常情況下,JavaScript 直譯器執行完try塊後,然後繼續執行finally塊,執行任何必要的清理工作。如果直譯器因為returncontinuebreak語句而離開try塊,那麼在直譯器跳轉到新目的地之前,將執行finally塊。

如果在try塊中發生異常,並且有一個關聯的catch塊來處理異常,直譯器首先執行catch塊,然後執行finally塊。如果沒有本地catch塊來處理異常,直譯器首先執行finally塊,然後跳轉到最近的包含catch子句。

如果finally塊本身導致使用returncontinuebreakthrow語句跳轉,或透過呼叫丟擲異常的方法,直譯器會放棄任何待處理的跳轉並執行新的跳轉。例如,如果finally子句丟擲異常,那個異常會替換正在被丟擲的任何異常。如果finally子句發出return語句,方法會正常返回,即使已經丟擲異常但尚未處理。

tryfinally可以在沒有catch子句的情況下一起使用。在這種情況下,finally塊只是保證會被執行的清理程式碼,無論try塊中發生了什麼。請記住,我們無法完全用while迴圈模擬for迴圈,因為continue語句對這兩種迴圈的行為是不同的。如果我們新增一個try/finally語句,我們可以編寫一個像for迴圈一樣工作並正確處理continue語句的while迴圈:

// Simulate for(*`initialize`* ; *`test`* ;*`increment`* ) body;
*`initialize`* ;
while( *`test`* ) {
    try { *`body`* ; }
    finally { *`increment`* ; }
}

但是請注意,包含break語句的bodywhile迴圈中的行為略有不同(導致在退出之前額外增加一次遞增)與在for迴圈中的行為不同,因此即使有finally子句,也無法完全用while模擬for迴圈。

5.6 其他語句

本節描述了剩餘的三個 JavaScript 語句——withdebugger"use strict"

5.6.1 with

with語句會將指定物件的屬性作為作用域內的變數執行一段程式碼塊。它的語法如下:

with (*`object`*)
    *`statement`*

這個語句建立一個臨時作用域,將object的屬性作為變數,然後在該作用域內執行statement

with語句在嚴格模式下是被禁止的(參見§5.6.3),在非嚴格模式下應被視為已棄用:儘量避免使用。使用with的 JavaScript 程式碼很難最佳化,並且可能比不使用with語句編寫的等效程式碼執行得慢得多。

with語句的常見用法是使得在深度巢狀的物件層次結構中更容易工作。例如,在客戶端 JavaScript 中,你可能需要輸入這樣的表示式來訪問 HTML 表單的元素:

document.forms[0].address.value

如果你需要多次編寫這樣的表示式,你可以使用with語句將表單物件的屬性視為變數處理:

with(document.forms[0]) {
    // Access form elements directly here. For example:
    name.value = "";
    address.value = "";
    email.value = "";
}

這樣可以減少你需要輸入的內容:你不再需要在每個表單屬性名稱前加上document.forms[0]。當然,避免使用with語句並像這樣編寫前面的程式碼同樣簡單:

let f = document.forms[0];
f.name.value = "";
f.address.value = "";
f.email.value = "";

請注意,如果在with語句的主體中使用constletvar宣告變數或常量,它會建立一個普通變數,而不會在指定物件中定義一個新屬性。

5.6.2 debugger

debugger語句通常不會執行任何操作。然而,如果一個偵錯程式程式可用且正在執行,那麼實現可能(但不是必須)執行某種除錯操作。實際上,這個語句就像一個斷點:JavaScript 程式碼的執行會停止,你可以使用偵錯程式列印變數的值,檢查呼叫堆疊等。例如,假設你在函式f()中遇到異常,因為它被使用未定義的引數呼叫,而你無法弄清楚這個呼叫是從哪裡來的。為了幫助你除錯這個問題,你可以修改f(),使其如下所示開始:

function f(o) {
  if (o === undefined) debugger;  // Temporary line for debugging purposes
  ...                             // The rest of the function goes here.
}

現在,當沒有引數呼叫f()時,執行會停止,你可以使用偵錯程式檢查呼叫堆疊,並找出這個錯誤呼叫是從哪裡來的。

請注意,僅僅擁有一個偵錯程式是不夠的:debugger語句不會為你啟動偵錯程式。然而,如果你正在使用一個網頁瀏覽器並且開啟了開發者工具控制檯,這個語句會導致斷點。

5.6.3 “use strict”

"use strict"是 ES5 中引入的指令。 指令不是語句(但足夠接近,以至於在此處記錄了"use strict")。 "use strict"指令和常規語句之間有兩個重要區別:

  • 它不包括任何語言關鍵字:該指令只是一個表示式語句,由一個特殊的字串文字(單引號或雙引號)組成。

  • 它只能出現在指令碼的開頭或函式體的開頭,在任何真實語句出現之前。

"use strict"指令的目的是指示隨後的程式碼(在指令碼或函式中)是嚴格程式碼。 如果指令碼有"use strict"指令,則指令碼的頂級(非函式)程式碼是嚴格程式碼。 如果函式體在嚴格程式碼中定義或具有"use strict"指令,則函式體是嚴格程式碼。 如果從嚴格程式碼呼叫eval()方法,則傳遞給eval()的程式碼是嚴格程式碼,或者如果程式碼字串包含"use strict"指令。 除了明確宣告為嚴格的程式碼外,class體(第九章)中的任何程式碼或 ES6 模組(§10.3)中的任何程式碼都自動成為嚴格程式碼。 這意味著如果所有 JavaScript 程式碼都編寫為模組,則所有程式碼都自動成為嚴格程式碼,您將永遠不需要使用顯式的"use strict"指令。

嚴格模式下執行嚴格模式。 嚴格模式是語言的受限子集,修復了重要的語言缺陷,並提供了更強的錯誤檢查和增強的安全性。 由於嚴格模式不是預設設定,仍然使用語言的不足遺留功能的舊 JavaScript 程式碼將繼續正確執行。 嚴格模式和非嚴格模式之間的區別如下(前三個特別重要):

  • 在嚴格模式下,不允許使用with語句。

  • 在嚴格模式下,所有變數必須宣告:如果將值分配給未宣告的變數、函式、函式引數、catch子句引數或全域性物件的屬性,則會丟擲 ReferenceError。(在非嚴格模式下,這將透過向全域性物件新增新屬性來隱式宣告全域性變數。)

  • 在嚴格模式下,作為函式呼叫的函式(而不是作為方法)的this值為undefined。(在非嚴格模式下,作為函式呼叫的函式始終將全域性物件作為其this值傳遞。)此外,在嚴格模式下,當使用call()apply()(§8.7.4)呼叫函式時,this值正好是傳遞給call()apply()的第一個引數的值。(在非嚴格模式下,nullundefined值將替換為全域性物件,非物件值將轉換為物件。)

  • 在嚴格模式下,對不可寫屬性的賦值和嘗試在不可擴充套件物件上建立新屬性會丟擲 TypeError。(在非嚴格模式下,這些嘗試會靜默失敗。)

  • 在嚴格模式下,傳遞給eval()的程式碼不能在呼叫者的範圍內宣告變數或定義函式,就像在非嚴格模式下那樣。 相反,變數和函式定義存在於為eval()建立的新作用域中。 當eval()返回時,此作用域將被丟棄。

  • 在嚴格模式下,函式中的 Arguments 物件(§8.3.3)儲存傳遞給函式的值的靜態副本。 在非嚴格模式下,Arguments 物件具有“神奇”的行為,其中陣列的元素和命名函式引數都指向相同的值。

  • 在嚴格模式下,如果delete運算子後跟未經限定的識別符號(如變數、函式或函式引數),則會丟擲 SyntaxError。(在非嚴格模式下,這樣的delete表示式不起作用並計算為false。)

  • 在嚴格模式下,嘗試刪除不可配置屬性會丟擲 TypeError。 (在非嚴格模式下,嘗試失敗,delete表示式的值為false。)

  • 在嚴格模式下,物件字面量定義具有相同名稱的兩個或更多屬性是語法錯誤。(在非嚴格模式下,不會發生錯誤。)

  • 在嚴格模式下,函式宣告具有兩個或更多具有相同名稱的引數是語法錯誤。(在非嚴格模式下,不會發生錯誤。)

  • 在嚴格模式下,不允許使用八進位制整數字面量(以 0 開頭且後面不跟 x)。(在非嚴格模式下,一些實現允許八進位制字面量。)

  • 在嚴格模式下,識別符號evalarguments被視為關鍵字,不允許更改它們的值。不能為這些識別符號分配值,將它們宣告為變數,將它們用作函式名稱,將它們用作函式引數名稱,或將它們用作catch塊的識別符號。

  • 在嚴格模式下,限制了檢查呼叫堆疊的能力。在嚴格模式函式內,arguments.callerarguments.callee都會丟擲 TypeError。嚴格模式函式還具有callerarguments屬性,當讀取時會丟擲 TypeError。(一些實現在非嚴格函式上定義這些非標準屬性。)

5.7 宣告

關鍵字constletvarfunctionclassimportexport在技術上不是語句,但它們看起來很像語句,因此本書非正式地將它們稱為語句,因此它們在本章中值得一提。

這些關鍵字更準確地描述為宣告而不是語句。我們在本章開頭說過語句“讓某事發生”。宣告用於定義新值併為其賦予我們可以用來引用這些值的名稱。它們本身並沒有做太多事情,但透過為值提供名稱,它們在重要意義上定義了程式中其他語句的含義。

當程式執行時,程式的表示式正在被評估,程式的語句正在被執行。程式中的宣告不會像語句一樣“執行”:相反,它們定義了程式本身的結構。可以粗略地將宣告視為在程式碼開始執行之前處理的程式部分。

JavaScript 宣告用於定義常量、變數、函式和類,並用於在模組之間匯入和匯出值。下一小節將給出所有這些宣告的示例。它們在本書的其他地方都有更詳細的介紹。

5.7.1 const、let 和 var

constletvar宣告在§3.10 中有介紹。在 ES6 及更高版本中,const宣告常量,let宣告變數。在 ES6 之前,var關鍵字是宣告變數的唯一方式,沒有辦法宣告常量。使用var宣告的變數的作用域是包含函式而不是包含塊。這可能導致錯誤,並且在現代 JavaScript 中,沒有理由使用var而不是let

const TAU = 2*Math.PI;
let radius = 3;
var circumference = TAU * radius;

5.7.2 function

function宣告用於定義函式,在第八章中有詳細介紹。(我們還在§4.3 中看到function,那裡它被用作函式表示式的一部分而不是函式宣告。)函式宣告如下所示:

function area(radius) {
    return Math.PI * radius * radius;
}

函式宣告建立一個函式物件並將其分配給指定的名稱—在這個例子中是area。 在程式的其他地方,我們可以透過使用這個名稱引用函式—並執行其中的程式碼。 JavaScript 程式碼塊中的函式宣告在程式碼執行之前被處理,並且函式名稱在整個程式碼塊中繫結到函式物件。 我們說函式宣告被“提升”,因為它就好像它們都被移動到它們所在的作用域的頂部一樣。 結果是呼叫函式的程式碼可以存在於程式中,在宣告函式的程式碼之前。

§12.3 描述了一種特殊型別的函式,稱為生成器。 生成器宣告使用function關鍵字,但後面跟著一個星號。 §13.3 描述了非同步函式,也是使用function關鍵字宣告的,但前面加上async關鍵字。

5.7.3 類

在 ES6 及更高版本中,class宣告建立一個新的類,併為其賦予一個我們可以用來引用它的名稱。 類在第九章中有詳細描述。 一個簡單的類宣告可能如下所示:

class Circle {
    constructor(radius) { this.r = radius; }
    area() { return Math.PI * this.r * this.r; }
    circumference() { return 2 * Math.PI * this.r; }
}

與函式不同,類宣告不會被提升,你不能在類宣告之前的程式碼中使用以這種方式宣告的類。

5.7.4 匯入和匯出

importexport宣告一起使用,使得在 JavaScript 程式碼的一個模組中定義的值可以在另一個模組中使用。 模組是具有自己全域性名稱空間的 JavaScript 程式碼檔案,完全獨立於所有其他模組。 一個值(如函式或類)在一個模組中定義後,只有透過export匯出並在另一個模組中使用import匯入,才能在另一個模組中使用。 模組是第十章的主題,importexport在§10.3 中有詳細介紹。

import指令用於從另一個 JavaScript 程式碼檔案中匯入一個或多個值,並在當前模組中為它們命名。 import指令有幾種不同的形式。 以下是一些示例:

import Circle from './geometry/circle.js';
import { PI, TAU } from './geometry/constants.js';
import { magnitude as hypotenuse } from './vectors/utils.js';

JavaScript 模組中的值是私有的,除非它們已經被明確匯出,否則不能被匯入到其他模組中。 export指令可以實現這一點:它宣告當前模組中定義的一個或多個值被匯出,因此可以被其他模組匯入。 export指令比import指令有更多的變體。 這是其中之一:

// geometry/constants.js
const PI = Math.PI;
const TAU = 2 * PI;
export { PI, TAU };

export關鍵字有時用作其他宣告的修飾符,從而形成一種複合宣告,同時定義一個常量、變數、函式或類並將其匯出。 當一個模組只匯出一個值時,通常使用特殊形式export default

export const TAU = 2 * Math.PI;
export function magnitude(x,y) { return Math.sqrt(x*x + y*y); }
export default class Circle { /* class definition omitted here */ }

5.8 JavaScript 語句總結

本章介紹了 JavaScript 語言的每個語句,總結在表 5-1 中。

表 5-1. JavaScript 語句語法

語句 目的
break 退出最內層迴圈或switch或從命名封閉語句中退出
case switch語句中標記一個語句
class 宣告一個類
const 宣告和初始化一個或多個常量
continue 開始最內層迴圈或命名迴圈的下一次迭代
debugger 偵錯程式斷點
default 標記switch語句中的預設語句
do/while while迴圈的替代方案
export 宣告可以被其他模組匯入的值
for 一個易於使用的迴圈
for/await 非同步迭代非同步迭代器的值
for/in 列舉物件的屬性名稱
for/of 列舉可迭代物件(如陣列)的值
function 宣告一個函式
if/else 根據條件執行一個語句或另一個
import 宣告在其他模組中定義的值的名稱
label breakcontinue給語句命名
let 宣告並初始化一個或多個塊作用域變數(新語法)
return 從函式中返回一個值
switch 多路分支到casedefault:標籤
throw 丟擲異常
try/catch/finally 處理異常和程式碼清理
“use strict” 將嚴格模式限制應用於指令碼或函式
var 宣告並初始化一個或多個變數(舊語法)
while 基本的迴圈結構
with 擴充套件作用域鏈(已棄用且在嚴格模式下禁止使用)
yield 提供一個要迭代的值;僅在生成器函式中使用

¹ case表示式在執行時評估的事實使得 JavaScript 的switch語句與 C、C++和 Java 的switch語句有很大不同(且效率較低)。在那些語言中,case表示式必須是相同型別的編譯時常量,並且switch語句通常可以編譯為高效的跳轉表

² 當我們考慮在§5.5.3 中的continue語句時,我們會發現這個while迴圈並不是for迴圈的精確等價。

相關文章