2022 年 6 月 19 日

迴圈:while 和 for

我們常常需要重複動作。

例如,依序從清單中輸出商品,或只是針對 1 到 10 的每個數字執行相同的程式碼。

迴圈是一種重複執行相同程式碼多次的方式。

for…of 和 for…in 迴圈

給進階讀者的簡短公告。

本文僅涵蓋基礎迴圈:whiledo..whilefor(..;..;..)

如果您是為了尋找其他類型的迴圈而來到本文,這裡有幾個指標

否則,請繼續閱讀。

「while」迴圈

while 迴圈具有以下語法

while (condition) {
  // code
  // so-called "loop body"
}

condition 為真時,將執行迴圈主體中的 code

例如,以下迴圈會在 i < 3 時輸出 i

let i = 0;
while (i < 3) { // shows 0, then 1, then 2
  alert( i );
  i++;
}

迴圈主體的單次執行稱為一次迭代。上述範例中的迴圈會進行三次迭代。

如果上述範例中沒有 i++,理論上迴圈將會永遠重複下去。實際上,瀏覽器會提供方法來停止此類迴圈,而在伺服器端 JavaScript 中,我們可以終止程序。

任何表達式或變數都可以成為迴圈條件,而不仅仅是比較:while 會評估條件並將其轉換為布林值。

例如,撰寫 while (i != 0) 的較短方式為 while (i)

let i = 3;
while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops
  alert( i );
  i--;
}
單行主體不需要大括號

如果迴圈主體只有一個陳述式,我們可以省略大括號 {…}

let i = 3;
while (i) alert(i--);

「do…while」迴圈

條件檢查可以使用 do..while 語法移到迴圈主體下方

do {
  // loop body
} while (condition);

迴圈會先執行主體,然後檢查條件,並且在為真的情況下,會重複執行。

例如

let i = 0;
do {
  alert( i );
  i++;
} while (i < 3);

此種語法形式僅應在您希望迴圈主體在條件為真時執行至少一次時使用。通常,較佳的選擇是另一種形式:while(…) {…}

「for」迴圈

for 迴圈較為複雜,但也是最常用的迴圈。

它看起來像這樣

for (begin; condition; step) {
  // ... loop body ...
}

讓我們透過範例瞭解這些部分的含義。以下迴圈會執行 alert(i),其中 i03(但不包括 3

for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2
  alert(i);
}

讓我們逐一檢視 for 陳述式

部分
開頭 let i = 0 在進入迴圈時執行一次。
條件 i < 3 在每次迴圈迭代之前檢查。如果為假,迴圈就會停止。
主體 alert(i) 在條件為真的情況下重複執行。
步驟 i++ 在每次迭代中,於主體後執行。

一般迴圈演算法運作方式如下

Run begin
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ ...

也就是說,begin 執行一次,然後進行迭代:在每次 condition 測試後,執行 bodystep

如果您不熟悉迴圈,可以回到範例並在紙上逐步重現其執行方式,這可能會有幫助。

以下是我們案例中確切發生的情況

// for (let i = 0; i < 3; i++) alert(i)

// run begin
let i = 0
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// ...finish, because now i == 3
內聯變數宣告

在此,「計數器」變數 i 直接在迴圈中宣告。這稱為「內聯」變數宣告。此類變數僅在迴圈內可見。

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // error, no such variable

我們可以使用現有變數,而非定義變數

let i = 0;

for (i = 0; i < 3; i++) { // use an existing variable
  alert(i); // 0, 1, 2
}

alert(i); // 3, visible, because declared outside of the loop

略過部分

for 的任何部分都可以略過。

例如,如果我們不需要在迴圈開始時執行任何動作,我們可以省略 begin

如下所示

let i = 0; // we have i already declared and assigned

for (; i < 3; i++) { // no need for "begin"
  alert( i ); // 0, 1, 2
}

我們也可以移除 step 部分

let i = 0;

for (; i < 3;) {
  alert( i++ );
}

這會讓迴圈與 while (i < 3) 相同。

我們實際上可以移除所有內容,建立一個無限迴圈

for (;;) {
  // repeats without limits
}

請注意,必須存在兩個 for 分號 ;。否則,會發生語法錯誤。

中斷迴圈

通常,當迴圈條件變為 false 時,迴圈會結束。

但是,我們可以使用特殊的 break 指令隨時強制結束。

例如,以下迴圈會要求使用者輸入一系列數字,當沒有輸入數字時「中斷」

let sum = 0;

while (true) {

  let value = +prompt("Enter a number", '');

  if (!value) break; // (*)

  sum += value;

}
alert( 'Sum: ' + sum );

如果使用者輸入空白列或取消輸入,break 指令會在行 (*) 中啟動。它會立即停止迴圈,將控制權傳遞給迴圈後的第 1 行。也就是 alert

「無限迴圈 + 視需要使用 break」的組合非常適合在迴圈條件必須在迴圈的開頭或結尾,而是在主體的中間甚至多個位置檢查的情況。

繼續下一個迭代

continue 指令是 break 的「精簡版」。它不會停止整個迴圈。相反地,它會停止目前的迭代並強制迴圈開始新的迭代(如果條件允許)。

如果我們已完成目前的迭代並希望繼續下一個迭代,我們可以使用它。

以下迴圈使用 continue 僅輸出奇數值

for (let i = 0; i < 10; i++) {

  // if true, skip the remaining part of the body
  if (i % 2 == 0) continue;

  alert(i); // 1, then 3, 5, 7, 9
}

對於 i 的偶數值,continue 指令會停止執行主體,並將控制權傳遞給 for 的下一個迭代(下一個數字)。因此,alert 僅會在奇數值時呼叫。

continue 指令有助於減少巢狀

顯示奇數值的迴圈看起來像這樣

for (let i = 0; i < 10; i++) {

  if (i % 2) {
    alert( i );
  }

}

從技術角度來看,這與上述範例相同。當然,我們可以使用 if 區塊來包裝程式碼,而不是使用 continue

但作為副作用,這會建立另一個巢狀層級(大括號內的 alert 呼叫)。如果 if 內的程式碼長於數行,則可能會降低整體可讀性。

沒有 break/continue 在「?」的右側

請注意,無法將非表達式的語法結構與三元運算子 ? 搭配使用。特別是,例如 break/continue 等指令不允許在那裡使用。

例如,如果我們採用這個程式碼

if (i > 5) {
  alert(i);
} else {
  continue;
}

…並使用問號重寫

(i > 5) ? alert(i) : continue; // continue isn't allowed here

…它會停止運作:出現語法錯誤。

這是另一個不使用問號運算子 ? 取代 if 的原因。

break/continue 的標籤

有時我們需要一次從多個巢狀迴圈中中斷。

例如,在以下程式碼中,我們對 ij 進行迴圈,提示輸入座標 (i, j)(0,0)(2,2)

for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // what if we want to exit from here to Done (below)?
  }
}

alert('Done!');

如果使用者取消輸入,我們需要一種方法來停止處理程序。

input 之後的普通 break 只會中斷內部迴圈。這還不夠,標籤來救援了!

標籤是迴圈前帶有冒號的識別碼

labelName: for (...) {
  ...
}

以下迴圈中的 break <labelName> 陳述式會中斷到標籤

outer: for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // if an empty string or canceled, then break out of both loops
    if (!input) break outer; // (*)

    // do something with the value...
  }
}

alert('Done!');

在上述程式碼中,break outer 向上尋找名為 outer 的標籤,並中斷該迴圈。

因此,控制權會直接從 (*) 轉到 alert('Done!')

我們也可以將標籤移到單獨一行

outer:
for (let i = 0; i < 3; i++) { ... }

continue 指令也可以與標籤搭配使用。在此情況下,程式碼執行會跳到標籤迴圈的下一次反覆運算。

標籤不允許「跳」到任何地方

標籤不允許我們跳到程式碼中的任意位置。

例如,不可能這樣做

break label; // jump to the label below (doesn't work)

label: for (...)

break 指令必須在程式碼區塊內。技術上來說,任何標籤程式碼區塊都可以,例如

label: {
  // ...
  break label; // works
  // ...
}

…雖然 99.9% 的時間 break 都用於迴圈內,如我們在上述範例中看到的。

continue 只能從迴圈內執行。

摘要

我們涵蓋了 3 種類型的迴圈

  • while – 每次迭代之前會檢查條件。
  • do..while – 每次迭代之後會檢查條件。
  • for (;;) – 每次迭代之前會檢查條件,可使用其他設定。

要建立一個「無限」迴圈,通常會使用 while(true) 結構。這種迴圈就像其他迴圈一樣,可以使用 break 指令停止。

如果我們不想在目前的迭代中執行任何動作,並想要轉到下一個迭代,可以使用 continue 指令。

break/continue 支援迴圈之前的標籤。標籤是 break/continue 離開巢狀迴圈並前往外層迴圈的唯一方式。

作業

重要性:3

這段程式碼發出的最後一個值是什麼?為什麼?

let i = 3;

while (i) {
  alert( i-- );
}

答案:1

let i = 3;

while (i) {
  alert( i-- );
}

每次迴圈迭代都會將 i 減少 1。檢查 while(i) 會在 i = 0 時停止迴圈。

因此,迴圈的步驟形成以下順序(「迴圈展開」)

let i = 3;

alert(i--); // shows 3, decreases i to 2

alert(i--) // shows 2, decreases i to 1

alert(i--) // shows 1, decreases i to 0

// done, while(i) check stops the loop
重要性:4

對於每次迴圈迭代,寫下它輸出的值,然後與解答進行比較。

兩個迴圈的 alert 顯示相同的值,還是不同?

  1. 前置形式 ++i

    let i = 0;
    while (++i < 5) alert( i );
  2. 後置形式 i++

    let i = 0;
    while (i++ < 5) alert( i );

此作業說明了後置/前置形式在用於比較時,可能會導致不同的結果。

  1. 從 1 到 4

    let i = 0;
    while (++i < 5) alert( i );

    第一個值是 i = 1,因為 ++i 會先遞增 i,然後傳回新的值。因此,第一個比較是 1 < 5,而 alert 顯示 1

    接著是 2, 3, 4… – 這些值會一個接著一個出現。比較總是使用遞增的值,因為 ++ 在變數之前。

    最後,i = 4 遞增為 5,比較 while(5 < 5) 失敗,迴圈停止。因此,不會顯示 5

  2. 從 1 到 5

    let i = 0;
    while (i++ < 5) alert( i );

    第一個值仍然是 i = 1i++ 的後置形式會遞增 i,然後傳回舊的值,因此比較 i++ < 5 會使用 i = 0(與 ++i < 5 相反)。

    alert 呼叫是分開的。這是另一個陳述式,在遞增和比較後執行。因此它取得目前的 i = 1

    然後依序是 2, 3, 4…

    我們在 i = 4 處停止。前置形式 ++i 會遞增它,並在比較中使用 5。但這裡我們有後置形式 i++。因此它將 i 遞增到 5,但傳回舊值。因此比較實際上是 while(4 < 5) – 為真,且控制權轉移到 alert

    i = 5 是最後一個,因為在下一步 while(5 < 5) 為假。

重要性:4

針對每個迴圈,寫下它將顯示哪些值。然後與答案比較。

兩個迴圈的 alert 是否會顯示相同的數值?

  1. 後置形式

    for (let i = 0; i < 5; i++) alert( i );
  2. 前置形式

    for (let i = 0; i < 5; ++i) alert( i );

答案:在兩種情況下,從 04

for (let i = 0; i < 5; ++i) alert( i );

for (let i = 0; i < 5; i++) alert( i );

這可以從 for 的演算法中輕易推論出來

  1. 在所有事情之前執行 i = 0 一次(開始)。
  2. 檢查條件 i < 5
  3. 如果為 true – 執行迴圈主體 alert(i),然後 i++

遞增 i++ 與條件檢查(2)是分開的。那只是一個不同的陳述式。

遞增傳回的值在此處未被使用,因此 i++++i 之間沒有差別。

重要性:5

使用 for 迴圈來輸出從 210 的偶數。

執行示範

for (let i = 2; i <= 10; i++) {
  if (i % 2 == 0) {
    alert( i );
  }
}

我們使用「模數」運算子 % 來取得餘數,並在此處檢查偶數。

重要性:5

改寫程式碼,將 for 迴圈改為 while,但不要改變其行為(輸出應保持相同)。

for (let i = 0; i < 3; i++) {
  alert( `number ${i}!` );
}
let i = 0;
while (i < 3) {
  alert( `number ${i}!` );
  i++;
}
重要性:5

撰寫一個迴圈,提示輸入大於 100 的數字。如果訪客輸入其他數字,請要求他們重新輸入。

迴圈必須要求輸入數字,直到訪客輸入大於 100 的數字,或取消輸入/輸入空白列。

在此我們可以假設訪客只會輸入數字。在此任務中,不需要針對非數字輸入實作特殊處理。

執行示範

let num;

do {
  num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);

迴圈 do..while 會重複執行,只要兩個檢查都是真。

  1. 檢查 num <= 100 – 也就是說,輸入的值仍不大於 100
  2. numnull 或空字串時,檢查 && num 為 false。然後 while 迴圈也會停止。

P.S. 如果 numnull,則 num <= 100true,因此,如果沒有第 2 個檢查,如果使用者按一下取消,迴圈將不會停止。需要這兩個檢查。

重要性:3

如果一個整數大於 1,且無法被 1 和它本身以外的任何數字整除,則稱為 質數

換句話說,如果 n > 1 只能被 1n 整除,則它是一個質數。

例如,5 是質數,因為它無法被 234 整除。

撰寫程式碼,輸出從 2n 區間內的質數。

對於 n = 10,結果將為 2,3,5,7

P.S. 程式碼應適用於任何 n,而不針對任何固定值進行微調。

有許多演算法可以執行這項工作。

我們使用巢狀迴圈

For each i in the interval {
  check if i has a divisor from 1..i
  if yes => the value is not a prime
  if no => the value is a prime, show it
}

使用標籤的程式碼

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // for each i...

  for (let j = 2; j < i; j++) { // look for a divisor..
    if (i % j == 0) continue nextPrime; // not a prime, go next i
  }

  alert( i ); // a prime
}

有許多空間可以最佳化。例如,我們可以從 2i 的平方根尋找除數。但無論如何,如果我們希望對於大區間真正有效率,我們需要改變方法,並依賴進階數學和複雜演算法,例如 二次篩選法一般數體篩選法 等。

教學課程地圖

留言

留言前請先閱讀這段文字…
  • 如果您有改進建議,請 提交 GitHub 問題 或提出 pull 要求,而不是留言。
  • 如果您無法理解文章中的某個部分,請加以說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行,請將其包裝在 <pre> 標籤中,若要插入 10 行以上,請使用沙盒 (plnkrjsbincodepen…)