Javascript 循環(huán):while 和 for

2023-02-17 10:37 更新

我們經常需要重復執(zhí)行一些操作。

例如,我們需要將列表中的商品逐個輸出,或者運行相同的代碼將數字 1 到 10 逐個輸出。

循環(huán) 是一種重復運行同一代碼的方法。

for…of 和 for…in 循環(huán)

給進階讀者的一個小提示。

本文僅涵蓋了基礎的循環(huán):while,do..while 和 for(..; ..; ..)

如果你閱讀本文是為了尋找其他類型的循環(huán),那么:

  • 用于遍歷對象屬性的 ?for..in? 循環(huán)請見:for…in
  • 用于遍歷數組和可迭代對象的循環(huán)分別請見:for…of 和 iterables。

否則,請繼續(xù)閱讀。

“while” 循環(huán)

while 循環(huán)的語法如下:

while (condition) {
  // 代碼
  // 所謂的“循環(huán)體”
}

當 condition 為真時,執(zhí)行循環(huán)體的 code。

例如,以下將循環(huán)輸出當 i < 3 時的 i 值:

let i = 0;
while (i < 3) { // 依次顯示 0、1 和 2
  alert( i );
  i++;
}

循環(huán)體的單次執(zhí)行叫作 一次迭代。上面示例中的循環(huán)進行了三次迭代。

如果上述示例中沒有 i++,那么循環(huán)(理論上)會永遠重復執(zhí)行下去。實際上,瀏覽器提供了阻止這種循環(huán)的方法,我們可以通過終止進程,來停掉服務器端的 JavaScript。

任何表達式或變量都可以是循環(huán)條件,而不僅僅是比較。在 while 中的循環(huán)條件會被計算,計算結果會被轉化為布爾值。

例如,while (i != 0) 可簡寫為 while (i)

let i = 3;
while (i) { // 當 i 變成 0 時,條件為假,循環(huán)終止
  alert( i );
  i--;
}

單行循環(huán)體不需要大括號

如果循環(huán)體只有一條語句,則可以省略大括號 {…}

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

“do…while” 循環(huán)

使用 do..while 語法可以將條件檢查移至循環(huán)體 下面

do {
  // 循環(huán)體
} while (condition);

循環(huán)首先執(zhí)行循環(huán)體,然后檢查條件,當條件為真時,重復執(zhí)行循環(huán)體。

例如:

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

這種形式的語法很少使用,除非你希望不管條件是否為真,循環(huán)體 至少執(zhí)行一次。通常我們更傾向于使用另一個形式:while(…) {…}

“for” 循環(huán)

for 循環(huán)更加復雜,但它是最常使用的循環(huán)形式。

for 循環(huán)看起來就像這樣:

for (begin; condition; step) {
  // ……循環(huán)體……
}

我們通過示例來了解一下這三個部分的含義。下述循環(huán)從 i 等于 0 到 3(但不包括 3)運行 alert(i)

for (let i = 0; i < 3; i++) { // 結果為 0、1、2
  alert(i);
}

我們逐個部分分析 for 循環(huán):

語句段
begin let i = 0 進入循環(huán)時執(zhí)行一次。
condition i < 3 在每次循環(huán)迭代之前檢查,如果為 false,停止循環(huán)。
body(循環(huán)體) alert(i) 條件為真時,重復運行。
step i++ 在每次循環(huán)體迭代后執(zhí)行。

一般循環(huán)算法的工作原理如下:

開始運行
→ (如果 condition 成立 → 運行 body 然后運行 step)
→ (如果 condition 成立 → 運行 body 然后運行 step)
→ (如果 condition 成立 → 運行 body 然后運行 step)
→ ...

所以,begin 執(zhí)行一次,然后進行迭代:每次檢查 condition 后,執(zhí)行 body 和 step。

如果你這是第一次接觸循環(huán),那么回到這個例子,在一張紙上重現它逐步運行的過程,可能會對你有所幫助。

以下是在這個示例中發(fā)生的事:

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

// 開始
let i = 0
// 如果條件為真,運行下一步
if (i < 3) { alert(i); i++ }
// 如果條件為真,運行下一步
if (i < 3) { alert(i); i++ }
// 如果條件為真,運行下一步
if (i < 3) { alert(i); i++ }
// ……結束,因為現在 i == 3

內聯變量聲明

這里“計數”變量 i 是在循環(huán)中聲明的。這叫做“內聯”變量聲明。這樣的變量只在循環(huán)中可見。

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // 錯誤,沒有這個變量。

除了定義一個變量,我們也可以使用現有的變量:

let i = 0;

for (i = 0; i < 3; i++) { // 使用現有的變量
  alert(i); // 0, 1, 2
}

alert(i); //3,可見,因為是在循環(huán)之外聲明的

省略語句段

for 循環(huán)的任何語句段都可以被省略。

例如,如果我們在循環(huán)開始時不需要做任何事,我們就可以省略 begin 語句段。

就像這樣:

let i = 0; // 我們已經聲明了 i 并對它進行了賦值

for (; i < 3; i++) { // 不再需要 "begin" 語句段
  alert( i ); // 0, 1, 2
}

我們也可以移除 step 語句段:

let i = 0;

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

該循環(huán)與 while (i < 3) 等價。

實際上我們可以刪除所有內容,從而創(chuàng)建一個無限循環(huán):

for (;;) {
  // 無限循環(huán)
}

請注意 for 的兩個 ; 必須存在,否則會出現語法錯誤。

跳出循環(huán)

通常條件為假時,循環(huán)會終止。

但我們隨時都可以使用 break 指令強制退出。

例如,下面這個循環(huán)要求用戶輸入一系列數字,在輸入的內容不是數字時“終止”循環(huán)。

let sum = 0;

while (true) {

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

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

  sum += value;

}
alert( 'Sum: ' + sum );

如果用戶輸入空行或取消輸入,在 (*) 行的 break 指令會被激活。它立刻終止循環(huán),將控制權傳遞給循環(huán)后的第一行,即,alert

根據需要,"無限循環(huán) + break" 的組合非常適用于不必在循環(huán)開始/結束時檢查條件,但需要在中間甚至是主體的多個位置進行條件檢查的情況。

繼續(xù)下一次迭代

continue 指令是 break 的“輕量版”。它不會停掉整個循環(huán)。而是停止當前這一次迭代,并強制啟動新一輪循環(huán)(如果條件允許的話)。

如果我們完成了當前的迭代,并且希望繼續(xù)執(zhí)行下一次迭代,我們就可以使用它。

下面這個循環(huán)使用 continue 來只輸出奇數:

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

  //如果為真,跳過循環(huán)體的剩余部分。
  if (i % 2 == 0) continue;

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

對于偶數的 i 值,continue 指令會停止本次循環(huán)的繼續(xù)執(zhí)行,將控制權傳遞給下一次 for 循環(huán)的迭代(使用下一個數字)。因此 alert 僅被奇數值調用。

?continue ?指令利于減少嵌套

顯示奇數的循環(huán)可以像下面這樣:

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

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

}

從技術角度看,它與上一個示例完全相同。當然,我們可以將代碼包裝在 if 塊而不使用 continue。

但在副作用方面,它多創(chuàng)建了一層嵌套(大括號內的 alert 調用)。如果 if 中代碼有多行,則可能會降低代碼整體的可讀性。

禁止 ?break/continue? 在 ‘?’ 的右邊

請注意非表達式的語法結構不能與三元運算符 ? 一起使用。特別是 break/continue 這樣的指令是不允許這樣使用的。

例如,我們使用如下代碼:

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

……用問號重寫:

(i > 5) ? alert(i) : continue; // continue 不允許在這個位置

……代碼會停止運行,并顯示有語法錯誤。

這是不(建議)使用問號 ? 運算符替代 if 語句的另一個原因。

break/continue 標簽

有時候我們需要一次從多層嵌套的循環(huán)中跳出來。

例如,下述代碼中我們的循環(huán)使用了 i 和 j,從 (0,0) 到 (3,3) 提示坐標 (i, j)

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

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

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

    // 如果我想從這里退出并直接執(zhí)行 alert('Done!')
  }
}

alert('Done!');

我們需要提供一種方法,以在用戶取消輸入時來停止這個過程。

在 input 之后的普通 break 只會打破內部循環(huán)。這還不夠 —— 標簽可以實現這一功能!

標簽 是在循環(huán)之前帶有冒號的標識符:

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

break <labelName> 語句跳出循環(huán)至標簽處:

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

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

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

    // 如果是空字符串或被取消,則中斷并跳出這兩個循環(huán)。
    if (!input) break outer; // (*)

    // 用得到的值做些事……
  }
}

alert('Done!');

上述代碼中,break outer 向上尋找名為 outer 的標簽并跳出當前循環(huán)。

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

我們還可以將標簽移至單獨一行:

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

continue 指令也可以與標簽一起使用。在這種情況下,執(zhí)行跳轉到標記循環(huán)的下一次迭代。

標簽并不允許“跳到”所有位置

標簽不允許我們跳到代碼的任意位置。

例如,這樣做是不可能的:

break label;  // 跳轉至下面的 label 處(無效)

label: for (...)

break 指令必須在代碼塊內。從技術上講,任何被標記的代碼塊都有效,例如:

label: {
  // ...
  break label; // 有效
  // ...
}

……盡管 99.9% 的情況下 break 都被用在循環(huán)內,就像在上面那些例子中我們看到的那樣。

continue 只有在循環(huán)內部才可行。

總結

我們學習了三種循環(huán):

  • ?while ?—— 每次迭代之前都要檢查條件。
  • ?do..while? —— 每次迭代后都要檢查條件。
  • ?for (;;)? —— 每次迭代之前都要檢查條件,可以使用其他設置。

通常使用 while(true) 來構造“無限”循環(huán)。這樣的循環(huán)和其他循環(huán)一樣,都可以通過 break 指令來終止。

如果我們不想在當前迭代中做任何事,并且想要轉移至下一次迭代,那么可以使用 continue 指令。

break/continue 支持循環(huán)前的標簽。標簽是 break/continue 跳出嵌套循環(huán)以轉到外部的唯一方法。

任務


最后一次循環(huán)的值

重要程度: 3

此代碼最后一次 alert 值是多少?為什么?

let i = 3;

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

解決方案

答案是:1。

let i = 3;

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

每次循環(huán)迭代都將 i 減 1。當檢查到 i = 0 時,while(i) 循環(huán)停止。

因此,此循環(huán)執(zhí)行的步驟如下(“循環(huán)展開”):

let i = 3;

alert(i--); // 顯示 3,i 減至 2

alert(i--) // 顯示 2,i 減至 1

alert(i--) // 顯示 1,i 減至 0

// 完成,while(i) 檢查循環(huán)條件并停止循環(huán)

while 循環(huán)顯示哪些值?

重要程度: 4

對于每次循環(huán),寫出你認為會顯示的值,然后與答案進行比較。

以下兩個循環(huán)的 alert 值是否相同?

  1. 前綴形式 ++i:
  2. let i = 0;
    while (++i < 5) alert( i );
  3. 后綴形式 i++
  4. let i = 0;
    while (i++ < 5) alert( i );

解決方案

這個題目展現了 i++/++i 兩種形式在比較中導致的不同結果。

  1. 從 1 到 4
  2. let i = 0;
    while (++i < 5) alert( i );

    第一個值是 i = 1,因為 ++i 首先遞增 i 然后返回新值。因此先比較 1 < 5 然后通過 alert 顯示 1

    然后按照 2, 3, 4… —— 數值一個接著一個被顯示出來。在比較中使用的都是遞增后的值,因為 ++ 在變量前。

    最終,i = 4 時,在 ++i < 5 的比較中,i 值遞增至 5,所以 while(5 < 5) 不符合循環(huán)條件,循環(huán)停止。所以沒有顯示 5

  3. 從 1 到 5
  4. let i = 0;
    while (i++ < 5) alert( i );

    第一個值也是 i = 1。后綴形式 i++ 遞增 i 然后返回  值,因此比較 i++ < 5 將使用 i = 0(與 ++i < 5 不同)。

    但 alert 調用是獨立的。這是在遞增和比較之后執(zhí)行的另一條語句。因此它得到了當前的 i = 1

    接下來是 2, 3,4…

    我們在 i = 4 時暫停,前綴形式 ++i 會遞增 i 并在比較中使用新值 5。但我們這里是后綴形式 i++。因此,它將 i 遞增到 5,但返回舊值。因此實際比較的是 while(4 < 5) —— true,程序繼續(xù)執(zhí)行 alert

    i = 5 是最后一個值,因為下一步比較 while(5 < 5) 為 false。


"for" 循環(huán)顯示哪些值?

重要程度: 4

對于每次循環(huán),寫下它將顯示的值。然后與答案進行比較。

兩次循環(huán) alert 值是否相同?

  1. 后綴形式
  2. for (let i = 0; i < 5; i++) alert( i );
  3. 前綴形式:
  4. for (let i = 0; i < 5; ++i) alert( i );

解決方案

答案:在這兩種情況下都是從 0 到 4

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

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

這可以很容易地從 for 算法中推導出:

  1. 在一切開始之前執(zhí)行 ?i = 0?。
  2. 檢查 ?i < 5? 條件
  3. 如果 ?true ?—— 執(zhí)行循環(huán)體并 ?alert(i)?,然后進行 ?i++?

遞增 ?i++? 與檢查條件(2)分開。這只是另一種寫法。

在這沒使用返回的遞增值,因此 ?i++? 和 ?++i?之間沒有區(qū)別。


使用 for 循環(huán)輸出偶數

重要程度: 5

使用 for 循環(huán)輸出從 2 到 10 的偶數。


解決方案

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

我們使用 “modulo” 運算符 % 來獲取余數,并檢查奇偶性。


用 "while" 替換 "for"

重要程度: 5

重寫代碼,在保證不改變其行為的情況下,將 for 循環(huán)更改為 while(輸出應保持不變)。

for (let i = 0; i < 3; i++) {
  alert( `number ${i}!` );
}

解決方案

let i = 0;
while (i < 3) {
  alert( `number ${i}!` );
  i++;
}

重復輸入,直到正確為止

重要程度: 5

編寫一個提示用戶輸入大于 100 的數字的循環(huán)。如果用戶輸入其他數值 —— 請他重新輸入。

循環(huán)一直在請求一個數字,直到用戶輸入了一個大于 100 的數字、取消輸入或輸入了一個空行為止。

在這我們假設用戶只會輸入數字。在本題目中,不需要對非數值輸入進行特殊處理。


解決方案

let num;

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

兩個檢查都為真時,繼續(xù)執(zhí)行 ?do..while? 循環(huán):

  1. 檢查 ?num <= 100? —— 即輸入值仍然不大于 ?100?。
  2. 當 ?num ?為 ?null ?或空字符串時,?&& num? 的結果為 false。那么 ?while ?循環(huán)也會停止。

P.S. 如果 ?num ?為 ?null?,那么 ?num <= 100? 為 ?true?。因此用戶單擊取消,如果沒有第二次檢查,循環(huán)就不會停止。兩次檢查都是必須的。


輸出素數(prime)

重要程度: 3

大于 1 且不能被除了 1 和它本身以外的任何數整除的整數叫做素數

換句話說,n > 1 且不能被 1 和 n 以外的任何數整除的整數,被稱為素數。

例如,5 是素數,因為它不能被 2、3 和 4 整除,會產生余數。

寫一個可以輸出 2 到 n 之間的所有素數的代碼。

當 n = 10,結果輸出 2、3、5、7。

P.S. 代碼應適用于任何 n,而不是對任何固定值進行硬性調整。


解決方案

這個題目有很多解法。

我們使用一個嵌套循環(huán):

對于間隔中的每個 i {
  檢查在 1~i 之間,是否有 i 的除數
  如果有 => 這個 i 不是素數
  如果沒有 => 這個 i 是素數,輸出出來
}

使用標簽的代碼:

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // 對每個自然數 i

  for (let j = 2; j < i; j++) { // 尋找一個除數……
    if (i % j == 0) continue nextPrime; // 不是素數,則繼續(xù)檢查下一個
  }

  alert( i ); // 輸出素數
}

這段代碼有很大的優(yōu)化空間。例如,我們可以從 2 到 i 的平方根之間的數中尋找除數。無論怎樣,如果我們想要在很大的數字范圍內實現高效率,我們需要改變實現方法,依賴高等數學和復雜算法,如二次篩選法(Quadratic sieve)普通數域篩選法(General number field sieve)等。

譯注:素數也稱為質數,對本答案的代碼進一步優(yōu)化,其實就是一道 LeetCode 算法題,感興趣的可以點擊鏈接查看如何通過 埃拉托斯特尼篩法篩選素數


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號