JavaScript 給心急的程式設計師(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

23 控制流程語句



本章節涵蓋以下控制流程語句

23.1 控制迴圈:breakcontinue

兩個運算子 breakcontinue 可用於控制迴圈和其他語句,當我們在它們裡面時。

23.1.1 break

break 有兩個版本:一個有運算元,一個沒有運算元。後者版本在以下語句中運作:whiledo-whileforfor-offor-await-offor-inswitch。它會立即離開目前的語句

for (const x of ['a', 'b', 'c']) {
  console.log(x);
  if (x === 'b') break;
  console.log('---')
}

// Output:
// 'a'
// '---'
// 'b'

23.1.2 break 加上標籤:離開任何標籤陳述式

break 帶有運算元,可在任何地方運作。其運算元為標籤。標籤可置於任何陳述式之前,包括區塊。break my_label 離開標籤為 my_label 的陳述式

my_label: { // label
  if (condition) break my_label; // labeled break
  // ···
}

在以下範例中,搜尋可以

function findSuffix(stringArray, suffix) {
  let result;
  search_block: {
    for (const str of stringArray) {
      if (str.endsWith(suffix)) {
        // Success:
        result = str;
        break search_block; // (A)
      }
    } // for
    // Failure:
    result = '(Untitled)'; // (B)
  } // search_block

  return { suffix, result };
    // Same as: {suffix: suffix, result: result}
}
assert.deepEqual(
  findSuffix(['notes.txt', 'index.html'], '.html'),
  { suffix: '.html', result: 'index.html' }
);
assert.deepEqual(
  findSuffix(['notes.txt', 'index.html'], '.mjs'),
  { suffix: '.mjs', result: '(Untitled)' }
);

23.1.3 continue

continue 僅在 whiledo-whileforfor-offor-await-offor-in 內運作。它會立即離開目前的迴圈反覆運算,並繼續下一個反覆運算 – 例如

const lines = [
  'Normal line',
  '# Comment',
  'Another normal line',
];
for (const line of lines) {
  if (line.startsWith('#')) continue;
  console.log(line);
}
// Output:
// 'Normal line'
// 'Another normal line'

23.2 控制流程陳述式的條件

ifwhiledo-while 具有原則上為布林的條件。然而,條件只需要真值(如果強制轉換為布林時為 true)即可被接受。換句話說,以下兩個控制流程陳述式是等效的

if (value) {}
if (Boolean(value) === true) {}

以下是所有假值的清單

所有其他值都是真值。如需更多資訊,請參閱 §15.2「假值和真值」

23.3 if 陳述式 [ES1]

以下是兩個簡單的 if 陳述式:一個只有「then」分支,另一個同時有「then」分支和「else」分支

if (cond) {
  // then branch
}

if (cond) {
  // then branch
} else {
  // else branch
}

else 不僅可以接續區塊,也可以接續另一個 if 陳述式

if (cond1) {
  // ···
} else if (cond2) {
  // ···
}

if (cond1) {
  // ···
} else if (cond2) {
  // ···
} else {
  // ···
}

您可以使用更多 else if 來繼續這個鏈。

23.3.1 if 陳述式的語法

if 陳述式的通用語法為

if (cond) «then_statement»
else «else_statement»

到目前為止,then_statement 一直都是一個區塊,但我們可以使用任何陳述式。該陳述式必須以分號結尾

if (true) console.log('Yes'); else console.log('No');

這表示 else if 不是它自己的結構;它只是一個 if 語句,其 else_statement 是另一個 if 語句。

23.4 switch 語句 [ES3]

switch 語句如下所示

switch («switch_expression») {
  «switch_body»
}

switch 的主體包含零個或多個 case 子句

case «case_expression»:
  «statements»

而且,選擇性地,一個 default 子句

default:
  «statements»

switch 的執行方式如下

23.4.1 switch 語句的第一個範例

我們來看一個範例:下列函式將 1-7 的數字轉換為星期幾的名稱。

function dayOfTheWeek(num) {
  switch (num) {
    case 1:
      return 'Monday';
    case 2:
      return 'Tuesday';
    case 3:
      return 'Wednesday';
    case 4:
      return 'Thursday';
    case 5:
      return 'Friday';
    case 6:
      return 'Saturday';
    case 7:
      return 'Sunday';
  }
}
assert.equal(dayOfTheWeek(5), 'Friday');

23.4.2 別忘了 returnbreak

在 case 子句的結尾,執行會繼續到下一個 case 子句,除非我們 returnbreak - 例如

function englishToFrench(english) {
  let french;
  switch (english) {
    case 'hello':
      french = 'bonjour';
    case 'goodbye':
      french = 'au revoir';
  }
  return french;
}
// The result should be 'bonjour'!
assert.equal(englishToFrench('hello'), 'au revoir');

也就是說,我們對 dayOfTheWeek() 的實作之所以有效,是因為我們使用了 return。我們可以使用 break 來修正 englishToFrench()

function englishToFrench(english) {
  let french;
  switch (english) {
    case 'hello':
      french = 'bonjour';
      break;
    case 'goodbye':
      french = 'au revoir';
      break;
  }
  return french;
}
assert.equal(englishToFrench('hello'), 'bonjour'); // ok

23.4.3 空的 case 子句

case 子句的語句可以省略,這實際上為我們提供了每一個 case 子句多個 case 表達式

function isWeekDay(name) {
  switch (name) {
    case 'Monday':
    case 'Tuesday':
    case 'Wednesday':
    case 'Thursday':
    case 'Friday':
      return true;
    case 'Saturday':
    case 'Sunday':
      return false;
  }
}
assert.equal(isWeekDay('Wednesday'), true);
assert.equal(isWeekDay('Sunday'), false);

23.4.4 透過 default 子句檢查非法值

如果 switch 表達式沒有其他匹配,就會跳到 default 子句。這使得它對錯誤檢查很有用

function isWeekDay(name) {
  switch (name) {
    case 'Monday':
    case 'Tuesday':
    case 'Wednesday':
    case 'Thursday':
    case 'Friday':
      return true;
    case 'Saturday':
    case 'Sunday':
      return false;
    default:
      throw new Error('Illegal value: '+name);
  }
}
assert.throws(
  () => isWeekDay('January'),
  {message: 'Illegal value: January'});

  練習:switch

23.5 while 迴圈 [ES1]

while 迴圈具有下列語法

while («condition») {
  «statements»
}

在每次迴圈迭代之前,while 會評估 condition

23.5.1 while 迴圈的範例

下列程式碼使用了 while 迴圈。在每次迴圈迭代中,它透過 .shift() 移除 arr 的第一個元素並記錄它。

const arr = ['a', 'b', 'c'];
while (arr.length > 0) {
  const elem = arr.shift(); // remove first element
  console.log(elem);
}
// Output:
// 'a'
// 'b'
// 'c'

如果條件總是評估為 true,則 while 是無限迴圈

while (true) {
  if (Math.random() === 0) break;
}

23.6 do-while 迴圈 [ES3]

do-while 迴圈的運作方式很像 while,但它是在每次迴圈迭代之後檢查其條件,而不是之前。

let input;
do {
  input = prompt('Enter text:');
  console.log(input);
} while (input !== ':q');

do-while 也可以視為至少執行一次的 while 迴圈。

prompt() 是一個在網路瀏覽器中可用的全域函式。它會提示使用者輸入文字並回傳它。

23.7 for 迴圈 [ES1]

for 迴圈有以下語法

for («initialization»; «condition»; «post_iteration») {
  «statements»
}

第一行是迴圈的開頭,它控制主體(迴圈的其餘部分)執行頻率。它有三個部分,每個部分都是可選的

因此,for 迴圈大致等於以下 while 迴圈

«initialization»
while («condition») {
  «statements»
  «post_iteration»
}

23.7.1 for 迴圈範例

例如,這是如何透過 for 迴圈從零數到二

for (let i=0; i<3; i++) {
  console.log(i);
}

// Output:
// 0
// 1
// 2

這是如何透過 for 迴圈記錄陣列的內容

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

// Output:
// 'a'
// 'b'
// 'c'

如果我們省略開頭的所有三個部分,就會得到一個無限迴圈

for (;;) {
  if (Math.random() === 0) break;
}

23.8 for-of 迴圈 [ES6]

for-of 迴圈會迭代任何可迭代的資料容器,它支援迭代協定。每個迭代的值都儲存在變數中,如開頭所指定

for («iteration_variable» of «iterable») {
  «statements»
}

迭代變數通常透過變數宣告建立

const iterable = ['hello', 'world'];
for (const elem of iterable) {
  console.log(elem);
}
// Output:
// 'hello'
// 'world'

但我們也可以使用已經存在的(可變)變數

const iterable = ['hello', 'world'];
let elem;
for (elem of iterable) {
  console.log(elem);
}

23.8.1 constfor-offor

請注意,在 for-of 迴圈中,我們可以使用 const。迭代變數仍然可以對每個迭代不同(它只是不能在迭代期間改變)。把它想成每次在新的範圍中執行新的 const 宣告。

相反地,在 for 迴圈中,如果變數的值會變動,我們必須透過 letvar 來宣告變數。

23.8.2 遍歷可迭代物件

如前所述,for-of 可用於任何可迭代物件,不只陣列,例如集合

const set = new Set(['hello', 'world']);
for (const elem of set) {
  console.log(elem);
}

23.8.3 遍歷陣列的 [索引, 元素] 成對

最後,我們也可以使用 for-of 來遍歷陣列的 [索引, 元素] 項目

const arr = ['a', 'b', 'c'];
for (const [index, elem] of arr.entries()) {
  console.log(`${index} -> ${elem}`);
}
// Output:
// '0 -> a'
// '1 -> b'
// '2 -> c'

使用 [index, element] 時,我們使用 解構 來存取陣列元素。

  練習:for-of

exercises/control-flow/array_to_string_test.mjs

23.9 for-await-of 迴圈 [ES2018]

for-await-of 類似於 for-of,但它用於非同步可迭代物件,而非同步可迭代物件。而且它只能用於非同步函式和非同步產生器中。

for await (const item of asyncIterable) {
  // ···
}

for-await-of非同步迭代章節 中有詳細說明。

23.10 for-in 迴圈(避免使用)[ES1]

for-in 迴圈會遍歷物件的所有(自有和繼承的)可列舉屬性金鑰。當遍歷陣列時,它很少是好的選擇

以下程式碼示範這些重點

const arr = ['a', 'b', 'c'];
arr.propKey = 'property value';

for (const key in arr) {
  console.log(key);
}

// Output:
// '0'
// '1'
// '2'
// 'propKey'

23.11 迴圈建議

  測驗

請參閱 測驗應用程式