第 13 章 陳述式
目錄
購買書籍
(廣告,請勿封鎖)

第 13 章 陳述式

本章涵蓋 JavaScript 的陳述式:變數宣告、迴圈、條件式和其他內容。

宣告和指派變數

var 用於宣告變數,這會建立變數,讓您可以使用它。 等號運算子 (=) 用於指派值給它:

var foo;
foo = 'abc';

var 也讓您可以將前兩個陳述式合併成一個陳述式

var foo = 'abc';

最後,您也可以將多個 var 陳述式合併成一個

var x, y=123, z;

第 16 章 中進一步了解變數的工作原理。

迴圈和條件式的本體

複合陳述式(例如迴圈和條件式)有一個或多個嵌入的「本體」—例如 while 迴圈:

while («condition»)
    «statement»

對於本體 «陳述式»,您可以選擇。您可以使用單一陳述式

while (x >= 0) x--;

或您可以使用區塊(計為單一陳述式):

while (x > 0) {
    x--;
}

如果您要讓本體包含多個陳述式,則需要使用區塊。除非完整的複合陳述式可以寫在一行中,否則我建議使用區塊。

迴圈

本節探討 JavaScript 的迴圈陳述式。

與迴圈一起使用的機制

下列機制可用於所有迴圈:

break ⟦«標籤»⟧
離開迴圈。
continue ⟦«標籤»⟧
停止目前的迴圈反覆,並立即繼續下一個反覆。
標籤

標籤是識別碼,後接冒號。在迴圈前面,標籤讓您可以中斷或繼續該迴圈,即使從巢狀在迴圈內的迴圈中也可以。在區塊前面,您可以中斷該區塊。在兩種情況下,標籤的名稱都會變成 breakcontinue 的引數。以下是中斷區塊的範例:

function findEvenNumber(arr) {
    loop: { // label
        for (var i=0; i<arr.length; i++) {
            var elem = arr[i];
            if ((elem % 2) === 0) {
                console.log('Found: ' + elem);
                break loop;
            }
        }
        console.log('No even number found.');
    }
    console.log('DONE');
}

while

A while 迴圈:

while («condition»)
    «statement»

只要 condition 成立,就執行 statement。如果 condition 永遠為 true,就會得到一個無限迴圈

while (true) { ... }

在以下範例中,我們移除陣列中的所有元素,並將它們記錄到主控台

var arr = [ 'a', 'b', 'c' ];
while (arr.length > 0) {
    console.log(arr.shift());
}

以下是輸出

a
b
c

do-while

A do-while 迴圈

do «statement»
while («condition»);

至少執行 statement 一次,然後只要 condition 成立,就繼續執行。例如

var line;
do {
    line = prompt('Enter a number:');
} while (!/^[0-9]+$/.test(line));

for

for 迴圈中

for (⟦«init»⟧; ⟦«condition»⟧; ⟦«post_iteration»⟧)
    «statement»

init 在迴圈之前執行一次,只要 conditiontrue,迴圈就會繼續執行。您可以在 init 中使用 var 來宣告變數,但這些變數的範圍永遠是完整的周圍函式。在迴圈的每次反覆運算之後,都會執行 post_iteration。考量所有這些,前面的迴圈等於以下 while 迴圈

«init»;
while («condition») {
    «statement»
    «post_iteration»;
}

以下範例是反覆運算陣列的傳統方式(其他可能性說明於 最佳實務:反覆運算陣列)

var arr = [ 'a', 'b', 'c' ];
for (var i=0; i<arr.length; i++) {
    console.log(arr[i]);
}

如果您省略頭部的所有部分,for 迴圈就會變成無限迴圈

for (;;) {
    ...
}

for-in

A for-in 迴圈

for («variable» in «object»)
    «statement»

反覆運算 object 的所有屬性金鑰,包括繼承的屬性。不過,會忽略標記為不可列舉的屬性(請參閱 屬性屬性和屬性描述詞)。以下規則適用於 for-in 迴圈

  • 您可以使用 var 來宣告變數,但這些變數的範圍總是完整的周圍函式。
  • 屬性可以在迭代期間刪除。

最佳實務:不要對陣列使用 for-in

不要使用 for-in迭代陣列。首先,它會迭代索引,而不是值:

> var arr = [ 'a', 'b', 'c' ];
> for (var key in arr) { console.log(key); }
0
1
2

其次,它也會迭代所有(非索引)屬性鍵。以下範例說明當您將屬性 foo 加入陣列時會發生什麼事

> var arr = [ 'a', 'b', 'c' ];
> arr.foo = true;
> for (var key in arr) { console.log(key); }
0
1
2
foo

因此,您最好使用一般的 for 迴圈或陣列方法 forEach()(請參閱 最佳實務:迭代陣列)。

最佳實務:小心對物件使用 for-in

for-in 迴圈會迭代 所有(可列舉的)屬性,包括繼承的屬性。這可能不是您想要的。讓我們使用以下建構函式來說明這個問題:

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Name: '+this.name;
};

Person 的執行個體會從 Person.prototype 繼承屬性 describe,而 for-in 會看到這個屬性

var person = new Person('Jane');
for (var key in person) {
    console.log(key);
}

以下是輸出

name
describe

一般來說,使用 for-in 的最佳方式是透過 hasOwnProperty() 來略過繼承的屬性

for (var key in person) {
    if (person.hasOwnProperty(key)) {
        console.log(key);
    }
}

以下是輸出

name

最後還有一個注意事項: person 可能有一個屬性 hasOwnProperty,這會讓檢查無法運作。為了安全起見,您必須直接參照一般方法(請參閱 一般方法:從原型借用方法Object.prototype.hasOwnProperty

for (var key in person) {
    if (Object.prototype.hasOwnProperty.call(person, key)) {
        console.log(key);
    }
}

還有其他更方便的方法可以迭代屬性鍵,這些方法會在 最佳實務:迭代自己的屬性 中說明。

for each-in

這個迴圈只存在於 Firefox。 不要使用它。

條件式

本節涵蓋 JavaScript 的條件式陳述。

if-then-else

if-then-else 敘述中

if («condition»)
    «then_branch»
else
    «else_branch»⟧

then_branchelse_branch 可以是單一敘述或敘述區塊(請參閱 迴圈和條件式的主體)。

串接 if 敘述

你可以 串接多個 if 敘述:

if (s1 > s2) {
    return 1;
} else if (s1 < s2) {
    return -1;
} else {
    return 0;
}

請注意,在前面的範例中,所有 else 分支都是單一敘述(if 敘述)。只允許 else 分支使用區塊的程式語言需要某種 else-if 分支來串接。

陷阱:懸宕的 else

下列範例中 else 分支稱為 懸宕,因為不明確它屬於哪一個 if 敘述:

if («cond1») if («cond2») «stmt1» else «stmt2»

以下是簡單的規則:使用大括號。 前面的程式片段等同於下列程式碼(其中 else 所屬的對象很明顯):

if («cond1») {
    if («cond2») {
        «stmt1»
    } else {
        «stmt2»
    }
}

switch

一個 switch 敘述:

switch («expression») {
    case «label1_1»:
    case «label1_2»:
        ...
        «statements1»
        break;
    case «label2_1»:
    case «label2_2»:
        ...
        «statements2»
        break;
    ...
    default:
        «statements_default»
        break;⟧⟧
}

評估 expression,然後跳到標籤與 switch 參數結果相符的 case 子句。 如果沒有標籤相符,switch 會跳到 default 子句(如果存在)或不會執行任何動作。

case 之後的「運算元」可以是任何運算式;它會透過 ===switch 的參數進行比較。

如果你沒有使用終止敘述完成子句,執行會繼續進行到下一個子句。最常使用的終止敘述是 break但是 returnthrow 也可以使用,即使它們通常會離開不只 switch 敘述。

下列範例說明如果你使用 throwreturn,就不需要 break

function divide(dividend, divisor) {
    switch (divisor) {
        case 0:
            throw 'Division by zero';
        default:
            return dividend / divisor;
    }
}

在此範例中,沒有 default 子句。因此,如果 fruit 與任何 case 標籤都不相符,就不會發生任何事

function useFruit(fruit) {
    switch (fruit) {
        case 'apple':
            makeCider();
            break;
        case 'grape':
            makeWine();
            break;
        // neither apple nor grape: do nothing
    }
}

在此範例中,有多個 case 標籤連續出現

function categorizeColor(color) {
    var result;
    switch (color) {
        case 'red':
        case 'yellow':
        case 'blue':
            result = 'Primary color: '+color;
            break;
        case 'orange':
        case 'green':
        case 'violet':
            result = 'Secondary color: '+color;
            break;
        case 'black':
        case 'white':
            result = 'Not a color';
            break;
        default:
            throw 'Illegal argument: '+color;
    }
    console.log(result);
}

此範例說明 case 之後的數值可以是任意運算式

function compare(x, y) {
    switch (true) {
        case x < y:
            return -1;
        case x === y:
            return 0;
        default:
            return 1;
    }
}

前述的 switch 陳述式會透過遍歷 case 子句,尋找與其參數 true 相符的項目。如果其中一個 case 表達式評估為 true,就會執行對應的 case 主體。因此,前述程式碼等同於下列 if 陳述式

function compare(x, y) {
    if (x < y) {
        return -1;
    } else if (x === y) {
        return 0;
    } else {
        return 1;
    }
}

您通常應該偏好後者解決方案,因為它較能說明本身。

with 陳述式

本節說明 with 陳述式在 JavaScript 中的運作方式,以及為何不建議使用它。

語法和語意

with 陳述式的語法如下

with («object»)
    «statement»

它會將 object 的屬性轉換為 statement 的區域變數。例如

var obj = { first: 'John' };
with (obj) {
    console.log('Hello '+first); // Hello John
}

其預期用途是避免在多次存取物件時產生重複。以下是包含重複的程式碼範例

foo.bar.baz.bla   = 123;
foo.bar.baz.yadda = 'abc';

with 會讓它變短

with (foo.bar.baz) {
    bla   = 123;
    yadda = 'abc';
}

with 陳述式已棄用

一般不建議使用 the with 陳述式(下一節說明原因)。例如,在嚴格模式中禁止使用:

> function foo() { 'use strict'; with ({}); }
SyntaxError: strict mode code may not contain 'with' statements

避免使用 with 陳述式的技巧

避免使用 類似這樣的程式碼:

// Don't do this:
with (foo.bar.baz) {
    console.log('Hello '+first+' '+last);
}

請改用具有簡短名稱的暫時變數

var b = foo.bar.baz;
console.log('Hello '+b.first+' '+b.last);

如果您不希望將暫時變數 b 公開到目前的範圍,您可以使用 IIFE(請參閱 透過 IIFE 引入新的範圍)

(function () {
    var b = foo.bar.baz;
    console.log('Hello '+b.first+' '+b.last);
}());

您也可以選擇將您想要存取的物件設為 IIFE 的參數

(function (b) {
    console.log('Hello '+b.first+' '+b.last);
}(foo.bar.baz));

棄用的理由

若要了解為何 with 已棄用,請查看下列範例,並注意函式的引數如何完全改變其運作方式:

function logMessage(msg, opts) {
    with (opts) {
        console.log('msg: '+msg); // (1)
    }
}

如果 opts 具有屬性 msg,則第 (1) 行的陳述式不再存取參數 msg。它存取屬性

> logMessage('hello', {})  // parameter msg
msg: hello
> logMessage('hello', { msg: 'world' })  // property opts.msg
msg: world

with 陳述式會造成三個問題

效能受損
變數查詢會變慢,因為一個物件暫時插入範圍鏈中。
程式碼變得較難預測

你無法透過查看識別項的語法環境(其詞彙內容)來判斷它所指為何。根據 Brendan Eich 的說法,這是 with 被棄用的真正原因,而不是效能考量

with 違反詞彙範圍,使得程式分析(例如,為了安全性)難以執行,甚至無法執行。

縮小程式(在 第 32 章 中說明)無法縮短變數名稱
with 陳述式內,你無法靜態地判斷名稱是指變數還是屬性。只有變數才能由縮小程式重新命名。

以下是一個 with 使程式碼脆弱的範例

function foo(someArray) {
    var values = ...;  // (1)
    with (someArray) {
        values.someMethod(...);  // (2)
        ...
    }
}
foo(myData);  // (3)

即使你無法存取陣列 myData,你也可以防止第 (3) 行的函式呼叫運作。

如何?透過新增一個屬性 valuesArray.prototype。例如

Array.prototype.values = function () {
    ...
};

現在第 (2) 行的程式碼呼叫 someArray.values.someMethod() 而不是 values.someMethod()。原因是,在 with 陳述式內,values 現在是指 someArray.values,而不是第 (1) 行的區域變數。

這不只是個思想實驗:陣列方法 values() 已新增到 Firefox,並中斷了 TYPO3 內容管理系統。 Brandon Benvie 找出問題所在

偵錯器陳述式

以下是 debugger 陳述式的語法

debugger;

如果偵錯器處於活動狀態,此陳述式會作為中斷點;如果沒有,則不會產生可觀察的效應。

下一頁:14. 例外處理