我們經常需要重複執行壹些操作。
例如,我們需要將列表中的商品逐個輸出,或者運行相同的代碼將數字 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 算法題,感興趣的可以點擊鏈接查看如何通過 埃拉托斯特尼篩法篩選素數。