我們經常需要重複執行壹些操作。
例如,我們需要將列表中的商品逐個輸出,或者運行相同的代碼將數字 1 到 10 逐個輸出。
循環 是壹種重複運行同壹代碼的方法。
for…of 和 for…in 循環
給進階讀者的壹個小提示。
本文僅涵蓋了基礎的循環:while,do..while 和 for(..; ..; ..)。
如果妳閱讀本文是爲了尋找其他類型的循環,那麽:
用于遍曆對象屬性的 for..in 循環請見:for…in。
用于遍曆數組和可叠代對象的循環分別請見:for…of 和 iterables。
否則,請繼續閱讀。
while 循環的語法如下:
while (condition) {
// 代碼
// 所謂的“循環體”
}當 condition 爲真時,執行循環體的 code。
例如,以下將循環輸出當 i < 3 時的 i 值:
let i = 0;
while (i < 3) { // 依次顯示 0、1 和 2
alert( i );
i++;
}循環體的單次執行叫作 壹次叠代。上面示例中的循環進行了三次叠代。
如果上述示例中沒有 i++,那麽循環(理論上)會永遠重複執行下去。實際上,浏覽器提供了阻止這種循環的方法,我們可以通過終止進程,來停掉服務器端的 JavaScript。
任何表達式或變量都可以是循環條件,而不僅僅是比較。在 while 中的循環條件會被計算,計算結果會被轉化爲布爾值。
例如,while (i != 0) 可簡寫爲 while (i):
let i = 3;
while (i) { // 當 i 變成 0 時,條件爲假,循環終止
alert( i );
i--;
}單行循環體不需要大括號
如果循環體只有壹條語句,則可以省略大括號 {…}:
let i = 3; while (i) alert(i--);
使用 do..while 語法可以將條件檢查移至循環體 下面:
do {
// 循環體
} while (condition);循環首先執行循環體,然後檢查條件,當條件爲真時,重複執行循環體。
例如:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);這種形式的語法很少使用,除非妳希望不管條件是否爲真,循環體 至少執行壹次。通常我們更傾向于使用另壹個形式:while(…) {…}。
for 循環更加複雜,但它是最常使用的循環形式。
for 循環看起來就像這樣:
for (begin; condition; step) {
// ……循環體……
}我們通過示例來了解壹下這三個部分的含義。下述循環從 i 等于 0 到 3(但不包括 3)運行 alert(i):
for (let i = 0; i < 3; i++) { // 結果爲 0、1、2
alert(i);
}我們逐個部分分析 for 循環:
| 語句段 | ||
|---|---|---|
| begin | let i = 0 | 進入循環時執行壹次。 |
| condition | i < 3 | 在每次循環叠代之前檢查,如果爲 false,停止循環。 |
| body(循環體) | alert(i) | 條件爲真時,重複運行。 |
| step | i++ | 在每次循環體叠代後執行。 |
壹般循環算法的工作原理如下:
開始運行 → (如果 condition 成立 → 運行 body 然後運行 step) → (如果 condition 成立 → 運行 body 然後運行 step) → (如果 condition 成立 → 運行 body 然後運行 step) → ...
所以,begin 執行壹次,然後進行叠代:每次檢查 condition 後,執行 body 和 step。
如果妳這是第壹次接觸循環,那麽回到這個例子,在壹張紙上重現它逐步運行的過程,可能會對妳有所幫助。
以下是在這個示例中發生的事:
// 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 是在循環中聲明的。這叫做“內聯”變量聲明。這樣的變量只在循環中可見。
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,可見,因爲是在循環之外聲明的for 循環的任何語句段都可以被省略。
例如,如果我們在循環開始時不需要做任何事,我們就可以省略 begin 語句段。
就像這樣:
let i = 0; // 我們已經聲明了 i 並對它進行了賦值
for (; i < 3; i++) { // 不再需要 "begin" 語句段
alert( i ); // 0, 1, 2
}我們也可以移除 step 語句段:
let i = 0;
for (; i < 3;) {
alert( i++ );
}該循環與 while (i < 3) 等價。
實際上我們可以刪除所有內容,從而創建壹個無限循環:
for (;;) {
// 無限循環
}請注意 for 的兩個 ; 必須存在,否則會出現語法錯誤。
通常條件爲假時,循環會終止。
但我們隨時都可以使用 break 指令強制退出。
例如,下面這個循環要求用戶輸入壹系列數字,在輸入的內容不是數字時“終止”循環。
let sum = 0;
while (true) {
let value = +prompt("Enter a number", '');
if (!value) break; // (*)
sum += value;
}
alert( 'Sum: ' + sum );如果用戶輸入空行或取消輸入,在 (*) 行的 break 指令會被激活。它立刻終止循環,將控制權傳遞給循環後的第壹行,即,alert。
根據需要,“無限循環 + break” 的組合非常適用于不必在循環開始/結束時檢查條件,但需要在中間甚至是主體的多個位置進行條件檢查的情況。
continue 指令是 break 的“輕量版”。它不會停掉整個循環。而是停止當前這壹次叠代,並強制啓動新壹輪循環(如果條件允許的話)。
如果我們完成了當前的叠代,並且希望繼續執行下壹次叠代,我們就可以使用它。
下面這個循環使用 continue 來只輸出奇數:
for (let i = 0; i < 10; i++) {
//如果爲真,跳過循環體的剩余部分。
if (i % 2 == 0) continue;
alert(i); // 1,然後 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 不允許在這個位置
……代碼會停止運行,並顯示有語法錯誤。
這是不(建議)使用問號 ? 運算符替代 if 語句的另壹個原因。
有時候我們需要壹次從多層嵌套的循環中跳出來。
例如,下述代碼中我們的循環使用了 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})`, '');
// 如果我想從這裏退出並直接執行 alert('Done!')
}
}
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 (!input) break outer; // (*)
// 用得到的值做些事……
}
}
alert('Done!');上述代碼中,break outer 向上尋找名爲 outer 的標簽並跳出當前循環。
因此,控制權直接從 (*) 轉至 alert('Done!')。
我們還可以將標簽移至單獨壹行:
outer:
for (let i = 0; i < 3; i++) { ... }continue 指令也可以與標簽壹起使用。在這種情況下,執行跳轉到標記循環的下壹次叠代。
標簽並不允許“跳到”所有位置
標簽不允許我們跳到代碼的任意位置。
例如,這樣做是不可能的:
break label; // 跳轉至下面的 label 處(無效) label: for (...)
break 指令必須在代碼塊內。從技術上講,任何被標記的代碼塊都有效,例如:
label: {
// ...
break label; // 有效
// ...
}……盡管 99.9% 的情況下 break 都被用在循環內,就像在上面那些例子中我們看到的那樣。
continue 只有在循環內部才可行。
我們學習了三種循環:
while —— 每次叠代之前都要檢查條件。
do..while —— 每次叠代後都要檢查條件。
for (;;) —— 每次叠代之前都要檢查條件,可以使用其他設置。
通常使用 while(true) 來構造“無限”循環。這樣的循環和其他循環壹樣,都可以通過 break 指令來終止。
如果我們不想在當前叠代中做任何事,並且想要轉移至下壹次叠代,那麽可以使用 continue 指令。
break/continue 支持循環前的標簽。標簽是 break/continue 跳出嵌套循環以轉到外部的唯壹方法。
重要程度: 3
此代碼最後壹次 alert 值是多少?爲什麽?
let i = 3;
while (i) {
alert( i-- );
}答案是:1。
let i = 3;
while (i) {
alert( i-- );
}每次循環叠代都將 i 減 1。當檢查到 i = 0 時,while(i) 循環停止。
因此,此循環執行的步驟如下(“循環展開”):
let i = 3; alert(i--); // 顯示 3,i 減至 2 alert(i--) // 顯示 2,i 減至 1 alert(i--) // 顯示 1,i 減至 0 // 完成,while(i) 檢查循環條件並停止循環
重要程度: 4
對于每次循環,寫出妳認爲會顯示的值,然後與答案進行比較。
以下兩個循環的 alert 值是否相同?
前綴形式 ++i:
let i = 0; while (++i < 5) alert( i );
後綴形式 i++
let i = 0; while (i++ < 5) alert( i );
這個題目展現了 i++/++i 兩種形式在比較中導致的不同結果。
從 1 到 4
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) 不符合循環條件,循環停止。所以沒有顯示 5。
從 1 到 5
let i = 0; while (i++ < 5) alert( i );
第壹個值也是 i = 1。後綴形式 i++ 遞增 i 然後返回 舊 值,因此比較 i++ < 5 將使用 i = 0(與 ++i < 5 不同)。
但 alert 調用是獨立的。這是在遞增和比較之後執行的另壹條語句。因此它得到了當前的 i = 1。
接下來是 2, 3,4…
我們在 i = 4 時暫停,前綴形式 ++i 會遞增 i 並在比較中使用新值 5。但我們這裏是後綴形式 i++。因此,它將 i 遞增到 5,但返回舊值。因此實際比較的是 while(4 < 5) —— true,程序繼續執行 alert。
i = 5 是最後壹個值,因爲下壹步比較 while(5 < 5) 爲 false。
重要程度: 4
對于每次循環,寫下它將顯示的值。然後與答案進行比較。
兩次循環 alert 值是否相同?
後綴形式:
for (let i = 0; i < 5; i++) alert( i );
前綴形式:
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 算法中推導出:
在壹切開始之前執行 i = 0。
檢查 i < 5 條件
如果 true —— 執行循環體並 alert(i),然後進行 i++
遞增 i++ 與檢查條件(2)分開。這只是另壹種寫法。
在這沒使用返回的遞增值,因此 i++ 和 ++i之間沒有區別。
重要程度: 5
使用 for 循環輸出從 2 到 10 的偶數。
運行 demo
for (let i = 2; i <= 10; i++) {
if (i % 2 == 0) {
alert( i );
}
}我們使用 “modulo” 運算符 % 來獲取余數,並檢查奇偶性。
重要程度: 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 的數字、取消輸入或輸入了壹個空行爲止。
在這我們假設用戶只會輸入數字。在本題目中,不需要對非數值輸入進行特殊處理。
運行 demo
let num;
do {
num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);兩個檢查都爲真時,繼續執行 do..while 循環:
檢查 num <= 100 —— 即輸入值仍然不大于 100。
當 num 爲 null 或空字符串時,&& num 的結果爲 false。那麽 while 循環也會停止。
P.S. 如果 num 爲 null,那麽 num <= 100 爲 true。因此用戶單擊取消,如果沒有第二次檢查,循環就不會停止。兩次檢查都是必須的。
重要程度: 3
大于 1 且不能被除了 1 和它本身以外的任何數整除的整數叫做素數。
換句話說,n > 1 且不能被 1 和 n 以外的任何數整除的整數,被稱爲素數。
例如,5 是素數,因爲它不能被 2、3 和 4 整除,會産生余數。
寫壹個可以輸出 2 到 n 之間的所有素數的代碼。
當 n = 10,結果輸出 2、3、5、7。
P.S. 代碼應適用于任何 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; // 不是素數,則繼續檢查下壹個
}
alert( i ); // 輸出素數
}這段代碼有很大的優化空間。例如,我們可以從 2 到 i 的平方根之間的數中尋找除數。無論怎樣,如果我們想要在很大的數字範圍內實現高效率,我們需要改變實現方法,依賴高等數學和複雜算法,如二次篩選法(Quadratic sieve),普通數域篩選法(General number field sieve)等。
譯注:素數也稱爲質數,對本答案的代碼進壹步優化,其實就是壹道 LeetCode 算法題,感興趣的可以點擊鏈接查看如何通過 埃拉托斯特尼篩法篩選素數。