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

2023-02-17 10:37 更新

我們經(jīng)常需要重復(fù)執(zhí)行一些操作。

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

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

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

給進(jìn)階讀者的一個(gè)小提示。

本文僅涵蓋了基礎(chǔ)的循環(huán):whiledo..while 和 for(..; ..; ..)。

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

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

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

“while” 循環(huán)

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

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

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

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

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

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

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

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

例如,while (i != 0) 可簡(jiǎn)寫(xiě)為 while (i)

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

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

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

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

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

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

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

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

例如:

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

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

“for” 循環(huán)

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

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

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

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

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

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

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

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

開(kāi)始運(yùn)行
→ (如果 condition 成立 → 運(yùn)行 body 然后運(yùn)行 step)
→ (如果 condition 成立 → 運(yùn)行 body 然后運(yùn)行 step)
→ (如果 condition 成立 → 運(yùn)行 body 然后運(yùn)行 step)
→ ...

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

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

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

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

// 開(kāi)始
let i = 0
// 如果條件為真,運(yùn)行下一步
if (i < 3) { alert(i); i++ }
// 如果條件為真,運(yùn)行下一步
if (i < 3) { alert(i); i++ }
// 如果條件為真,運(yùn)行下一步
if (i < 3) { alert(i); i++ }
// ……結(jié)束,因?yàn)楝F(xiàn)在 i == 3

內(nèi)聯(lián)變量聲明

這里“計(jì)數(shù)”變量 i 是在循環(huán)中聲明的。這叫做“內(nèi)聯(lián)”變量聲明。這樣的變量只在循環(huán)中可見(jiàn)。

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

除了定義一個(gè)變量,我們也可以使用現(xiàn)有的變量:

let i = 0;

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

alert(i); //3,可見(jiàn),因?yàn)槭窃谘h(huán)之外聲明的

省略語(yǔ)句段

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

例如,如果我們?cè)谘h(huán)開(kāi)始時(shí)不需要做任何事,我們就可以省略 begin 語(yǔ)句段。

就像這樣:

let i = 0; // 我們已經(jīng)聲明了 i 并對(duì)它進(jìn)行了賦值

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

我們也可以移除 step 語(yǔ)句段:

let i = 0;

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

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

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

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

請(qǐng)注意 for 的兩個(gè) ; 必須存在,否則會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤。

跳出循環(huán)

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

但我們隨時(shí)都可以使用 break 指令強(qiáng)制退出。

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

let sum = 0;

while (true) {

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

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

  sum += value;

}
alert( 'Sum: ' + sum );

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

根據(jù)需要,"無(wú)限循環(huán) + break" 的組合非常適用于不必在循環(huán)開(kāi)始/結(jié)束時(shí)檢查條件,但需要在中間甚至是主體的多個(gè)位置進(jìn)行條件檢查的情況。

繼續(xù)下一次迭代

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

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

下面這個(gè)循環(huán)使用 continue 來(lái)只輸出奇數(shù):

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

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

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

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

?continue ?指令利于減少嵌套

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

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

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

}

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

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

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

請(qǐng)注意非表達(dá)式的語(yǔ)法結(jié)構(gòu)不能與三元運(yùn)算符 ? 一起使用。特別是 break/continue 這樣的指令是不允許這樣使用的。

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

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

……用問(wèn)號(hào)重寫(xiě):

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

……代碼會(huì)停止運(yùn)行,并顯示有語(yǔ)法錯(cuò)誤。

這是不(建議)使用問(wèn)號(hào) ? 運(yùn)算符替代 if 語(yǔ)句的另一個(gè)原因。

break/continue 標(biāo)簽

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

例如,下述代碼中我們的循環(huán)使用了 i 和 j,從 (0,0) 到 (3,3) 提示坐標(biāo) (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!');

我們需要提供一種方法,以在用戶(hù)取消輸入時(shí)來(lái)停止這個(gè)過(guò)程。

在 input 之后的普通 break 只會(huì)打破內(nèi)部循環(huán)。這還不夠 —— 標(biāo)簽可以實(shí)現(xiàn)這一功能!

標(biāo)簽 是在循環(huán)之前帶有冒號(hào)的標(biāo)識(shí)符:

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

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

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

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

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

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

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

alert('Done!');

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

因此,控制權(quán)直接從 (*) 轉(zhuǎn)至 alert('Done!')。

我們還可以將標(biāo)簽移至單獨(dú)一行:

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

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

標(biāo)簽并不允許“跳到”所有位置

標(biāo)簽不允許我們跳到代碼的任意位置。

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

break label;  // 跳轉(zhuǎn)至下面的 label 處(無(wú)效)

label: for (...)

break 指令必須在代碼塊內(nèi)。從技術(shù)上講,任何被標(biāo)記的代碼塊都有效,例如:

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

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

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

總結(jié)

我們學(xué)習(xí)了三種循環(huán):

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

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

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

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

任務(wù)


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

重要程度: 3

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

let i = 3;

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

解決方案

答案是:1。

let i = 3;

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

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

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

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

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

以下兩個(gè)循環(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 );

解決方案

這個(gè)題目展現(xiàn)了 i++/++i 兩種形式在比較中導(dǎo)致的不同結(jié)果。

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

    第一個(gè)值是 i = 1,因?yàn)?nbsp;++i 首先遞增 i 然后返回新值。因此先比較 1 < 5 然后通過(guò) alert 顯示 1

    然后按照 2, 3, 4… —— 數(shù)值一個(gè)接著一個(gè)被顯示出來(lái)。在比較中使用的都是遞增后的值,因?yàn)?nbsp;++ 在變量前。

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

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

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

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

    接下來(lái)是 2, 3,4…

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

    i = 5 是最后一個(gè)值,因?yàn)橄乱徊奖容^ while(5 < 5) 為 false。


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

重要程度: 4

對(duì)于每次循環(huán),寫(xiě)下它將顯示的值。然后與答案進(jì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 算法中推導(dǎo)出:

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

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

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


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

重要程度: 5

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


解決方案

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

我們使用 “modulo” 運(yùn)算符 % 來(lái)獲取余數(shù),并檢查奇偶性。


用 "while" 替換 "for"

重要程度: 5

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

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

解決方案

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

重復(fù)輸入,直到正確為止

重要程度: 5

編寫(xiě)一個(gè)提示用戶(hù)輸入大于 100 的數(shù)字的循環(huán)。如果用戶(hù)輸入其他數(shù)值 —— 請(qǐng)他重新輸入。

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

在這我們假設(shè)用戶(hù)只會(huì)輸入數(shù)字。在本題目中,不需要對(duì)非數(shù)值輸入進(jìn)行特殊處理。


解決方案

let num;

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

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

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

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


輸出素?cái)?shù)(prime)

重要程度: 3

大于 1 且不能被除了 1 和它本身以外的任何數(shù)整除的整數(shù)叫做素?cái)?shù)。

換句話(huà)說(shuō),n > 1 且不能被 1 和 n 以外的任何數(shù)整除的整數(shù),被稱(chēng)為素?cái)?shù)。

例如,5 是素?cái)?shù),因?yàn)樗荒鼙?nbsp;2、3 和 4 整除,會(huì)產(chǎn)生余數(shù)。

寫(xiě)一個(gè)可以輸出 2 到 n 之間的所有素?cái)?shù)的代碼。

當(dāng) n = 10,結(jié)果輸出 2、3、5、7。

P.S. 代碼應(yīng)適用于任何 n,而不是對(duì)任何固定值進(jìn)行硬性調(diào)整。


解決方案

這個(gè)題目有很多解法。

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

對(duì)于間隔中的每個(gè) i {
  檢查在 1~i 之間,是否有 i 的除數(shù)
  如果有 => 這個(gè) i 不是素?cái)?shù)
  如果沒(méi)有 => 這個(gè) i 是素?cái)?shù),輸出出來(lái)
}

使用標(biāo)簽的代碼:

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // 對(duì)每個(gè)自然數(shù) i

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

  alert( i ); // 輸出素?cái)?shù)
}

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

譯注:素?cái)?shù)也稱(chēng)為質(zhì)數(shù),對(duì)本答案的代碼進(jìn)一步優(yōu)化,其實(shí)就是一道 LeetCode 算法題,感興趣的可以點(diǎn)擊鏈接查看如何通過(guò) 埃拉托斯特尼篩法篩選素?cái)?shù)。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)