30 天 Javascript 從入門到進階:迴圈
- 2018-11-11
- Liu, An-Chi 劉安齊
迴圈
在生活中,我們常常會有有做一樣的事情的時候,例如一張一張比對發票、一行一行檢查有沒有錯字。這個要重複做的動作,在程式語言中就可以用迴圈(loop)辦到。
傳送門:此系列文章「30 天 Javascript 從入門到進階」
¶ 迴圈概念
迴圈簡單來說就是讓程式在你指定的條件下,一直反覆地執行。
舉個例子,小男孩寫情書給女朋友,他想要寫十行「我愛妳」,一個一個打字的話,你就要打三十個字。你可能會想,沒關係啊,我可以複製貼上,但假設今天想要有一千行的「我愛妳」,即使是複製貼上你也會手酸。
所以這時候我們就可以用迴圈來執行這個重複的動作,我們可以寫個程式:
讓迴圈執行 10 遍:
印出「我愛你」
我們已經跟電腦說,迴圈跑 10 遍,這是我們給迴圈的範圍條件。記住,若是沒有給範圍條件,迴圈就會永遠跑下去。如果你發現迴圈沒有停下來,通常這代表你的程式邏輯有問題,因為我們通常不允許這種事情發生。無窮迴圈通常會被系統強制終結,最糟的情況下會導致當機。
我們知道這段程式碼會執行迴圈的內容十次,所以就會印出「我愛妳」十遍:
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
我愛妳
如果你想要有一千遍,只要在條件範圍指定 1000 遍即可。
又或者,假設我們要計算 1 加到 100 等於多少,我們用虛擬的程式來描述看看:
sum = 0
令 i 從 0 數到 100 跑一遍:
每次執行 sum 加上 i(意即 sum 為目前的 sum 加 i 得到的值)
這個虛擬碼就會開始執行迴圈,我們已經限制住迴圈的條件了,條件是 i
從 0 數到 100,所以 i
會從 0、1、2 一直數到 100,然後就停止了。每個 i
就是迴圈的一回合,會執行迴圈裡面的程式碼,在這邊就是 sum
加上 i
。
所以執行的過程會長這樣:
i | sum | 執行完的 sum |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
2 | 1 | 3 |
3 | 3 | 6 |
4 | 6 | 10 |
… | … | … |
100 | 4950 | 5050 |
當這個迴圈跑完之後,我們便得到了從 1 數到 100 的總和是多少。
¶ JS 中的迴圈
目前為止我們已經迴圈的大致用法,接著我們來看看要如何用 JS 來實現迴圈。
¶ For 迴圈
先從簡單的開始,如同剛剛範例,如何用 JS 印出 5 遍「我愛妳」呢?
檔案:ex1.js
for(let i = 0; i < 5; i++) {
console.log("I LOVE YOU!");
}
執行 ex1.js
就會看到以下結果:
$ node ex1.js
I LOVE YOU!
I LOVE YOU!
I LOVE YOU!
I LOVE YOU!
I LOVE YOU!
這邊開始解釋 for(let i = 0; i < 5; i++)
是什麼意思。在 JS 中 for
是一種迴圈的語法,用法是 for(定義變數;對變數的條件;變數動作)
,首先我們定義變數,然後告訴迴圈變數的條件是什麼,最後告訴迴圈每次執行要對變數做什麼。
以這邊為例,我們先定義 let i = 0
,然後規定 i < 5
,一但 i 大於等於 5 迴圈就會終止,最後告訴迴圈每一回合結束 i++
。i++
的意思就是 i = i + 1
,也就是每回合 i
會加 1。所以 i
其實就是一個計數器。
最後這段程式碼的 i
會從 0、1、2 一直數到 4,總共 5 次。當第 5 次執行完,i
會變成 5 就不滿足條件,迴圈就會結束了。
習慣上我們會讓計數器從 0 開始,因為後面會介紹的陣列通常都是由 0 開始,這邊可以先當作是慣例就好。當然,我們也可以讓 i
從 1 開始到 5 結束,迴圈就會是 for(let i = 1; i <= 5; i++)
。
for
後面包住的 {}
為迴圈每回合執行的程式碼,在這邊就是執行印出的動作。
再舉一個例子,我們也可以讓迴圈執行的內容與計數器有關:
檔案:ex2.js
for(let i = 0; i < 5; i++) {
console.log(i);
}
執行 ex2.js
就會看到以下結果:
$ node ex1.js
0
1
2
3
4
不管是我們單純的想要跑 n 遍而已,或是想要在迴圈內執行有關計數器的動作,都可以用 for
定義計數器 i
從初始值走到條件終點。
計數器當然也可以反向來數:
檔案:ex3.js
for(let i = 5; i > 0; i--) {
console.log(i);
}
我們先讓 i
為 5,接著每次 i--
等同 i = i - 1
,也就是每次減一,條件是必須大於 0。
執行 ex3.js
就會看到以下結果:
$ node ex1.js
5
4
3
2
1
還記得我們前面有用虛擬程式碼,計算 0 加到 100 等於多少,現在我們可以用 JS 來寫:
檔案:ex4.js
let sum = 0
for(let i = 0; i <= 100; i++) {
sum += i;
}
console.log(sum);
執行 ex4.js
就會看到以下結果:
$ node ex1.js
5050
我們必須把 sum
放在最一開始,也就是迴圈前面,這樣每回合才能使用 sum
。如果 sum
沒有在開始迴圈前就先定義,而是在迴圈的每一個回合中才被定義的話,那麼當那個回合結束, sum
也就消失了,因為迴圈裡面的變數的生命只存在當下的回合。
除非先把變數定義在迴圈外面,這樣的話每回合都是操作外面的變數。即使回合結束,外面的變數還會保留著。這個概念就是變數的生命週期(scope),與 if
、else
雷同,如同前一章所介紹。如果變數是在某個區塊 { }
裡面定義,變數的生命只在那個區塊 { }
的區間裡面存在,一但出了 { }
就會被銷毀。在迴圈中,每一回合就是一個 { }
,下一回合等同離開上一回合的 { }
。
所以假設我們這樣寫:
for(let i = 0; i <= 100; i++) {
let sum = i;
}
這樣寫就完全沒意義,因為每回合 sum
都會被重新定義,所以甚至辦不到累加動作!這就是為什麼會先在迴圈前先定義好 sum = 0
。
然後,我們讓 i
從 0 數到 100,每回合執行 sum += i
,+=
是一種縮寫,這句等於 sum = sum + i
,所以每回合就會把 i
加進 sum
裡面。最後迴圈結束之後,我們再印出 sum
是多少。
¶ while 迴圈
JS 中迴圈除了 for
以外,還可以使用 while
語法。兩個概念是一樣的,都是會在條件內反覆執行,不過使用方法有點不同,接下來我們比對 for
和 while
兩者即能看出差別。
舉個例子,舉個例子我們先用 for
來寫一個簡單迴圈:
檔案:ex5-1.js
for(let i = 0; i < 5; i++) {
console.log(i);
}
如果使用 while
的話,一樣的效果會寫成這樣:
檔案:ex5-2.js
let i = 0;
while(i < 5) {
console.log(i);
i += 1;
}
兩個結果一樣,執行 ex5-1.js
或 ex5-2.js
都會印出:
$ node ex5-2.js
0
1
2
3
4
while
的用法是 while(條件) { 做事 }
,在這邊 i < 5
是條件,每回合會先印出 i
然後再讓 i
加一,然後當 i = 4
的那一輪回合結束時,i
會變成 5,下一回合執行前會確認 i < 5
,這時候就發現條件不合,於是迴圈就會終止。要注意 i += 1
的位置很重要,如果先讓 i += 1
才印出 console.log(i)
,那麼印出的數字就會是「1 2 3 4 5」了。
從這邊我們可以發現 while 的特性是,控制條件的變數通常會在迴圈開始之前就存在,迴圈開始時,只會定義條件,不會初始化變數,這跟 for 會在迴圈一開始定義變數的做法不一樣。
也因為這個特性,while 通常會用在判斷「開關」的用途,例如:
檔案:ex6.js
let isOn = true;
while(isOn) { // 等同 while(isOn == true)
// 開著要做的事
const result = foo();
if(result == "the end"){
isOn = false;
}
}
ex6.js
只是情境,這個範例中,迴圈開始前有個開關變數 isOn
,接著 while
迴圈的條件是 isOn == true
,然後這個迴圈就會反覆的執行。
這邊有用到函數的概念,我們之後會提,可以先想成每次 result
會問 foo()
答案,然後把自己記錄成答案。
接著迴圈會一直跑,直到某一個回合 result
從 foo()
函數得到的結果是結束了,這時候 isOn
會被設為 false
,下一回合開始前檢查條件的時候就會不合,迴圈就結束了。
當然我們也可以用來單純地計數,如同先前 for 的範例,從 0 加到 100 可以這樣寫:
檔案:ex7.js
let i = 0;
let sum = 0;
while(i <= 100) {
sum += i; // 等同 sum = sum + i
i += 1;
}
console.log(sum);
這個效果跟之前的 for
是一模一樣的,所以其實同樣的邏輯 while
或 for
都辦得到,要用哪個端看使用者的心情。
不過你知道 ex7.js
最後的 i
會是多少嗎?若是我們在最後一行之後,再加上 console.log(i);
會印出多少數字呢?可以實驗看看並想想為什麼。
¶ 迴圈操作
再來要介紹迴圈中兩種特別的指令 break
& continue
。break
是可以直接中斷迴圈的指令,continue
則是可以讓迴圈跳過這一輪直接執行下一輪的指令。
¶ break
想像你今天有一百件褲子,你只知道上次不小心把耳機放在某一件褲子的口袋,這時候你去一件一件檢查褲子,只要途中哪一件褲子裡面發現耳機了,接下來就可以不用檢查了。
這就是 break 概念啦!我們通常用在迴圈中已經達到目的或是發現已經無法滿足目的,所以就讓迴圈停止,因為剩下的沒必要執行了。
舉例來說剛剛找耳機就是已經達成目的,所以剩下不用執行;想像一下你要把剛煮好的綠豆湯一碗一碗盛好,但一旦發現其中一碗裡面有死掉的蟲蟲,這一鍋的綠豆湯你可能都不想喝了,也就沒有必要繼續盛下一碗。
舉個簡單的範例:
檔案:ex8.js
while(true) {
const result = foo();
if(result == "find it"){
console.log("找到了,結束迴圈")
break;
}
}
ex8.js
只是概念,和 ex6.js
有異曲同工之妙,兩者皆是得到答案之後,就結束迴圈。只是 ex8.js
使用 break
來做跳出迴圈,而 ex6.js
是用一個開關變數來控制要不要結束迴圈。該採取哪一種做法,要看當時程式的先後文和情境,但效果其實是一樣的。
或是假設迴圈要從 0 數到 100 代表找耳機,所們先假設他數到 50 找到了,所以就可以結束迴圈,就會這樣:
檔案:ex9.js
for(let i = 0; i <= 100; i++){
console.log(`正在找第 ${i} 件褲子`);
if(i == 50){
console.log("找到了,結束迴圈")
break;
}
}
這邊有用到一個語法 `brabra ${var} brabra`。${}
裡面包變數,整句被 “`” 包住,就可以直接在字串中塞入變數。
執行 ex9.js
就會看到:
$ node ex9.js
正在找第 0 件褲子
正在找第 1 件褲子
正在找第 2 件褲子
正在找第 3 件褲子
...
...
正在找第 50 件褲子
找到了,結束迴圈
可以發現在 i
為 50 時因為 if
條件通過,所以執行 break
使迴圈終止。這個用法我們在介紹陣列時會再提一次。
¶ continue
想像一下你今天有一百個包包,假設每個包包有三個夾層,你要確認每個包包至少有一個夾層有放衛生紙,而你檢查的順序前、中、後夾層。也就是說假設你在前夾層看到有放衛生紙了,中、後兩層就可以跳過,然後去檢查下一個包包。
這種在迴圈中,在其中一個回合略過該回合接下來的工作,直接去執行下一回合,可以用 continue
指令。
檔案:ex10.js
for(let i = 0; i <= 100; i++){
console.log(`正在找第 ${i} 個包包`);
if(前面有){
console.log("找到了,跳下一回合")
continue;
}
if(中間有){
console.log("找到了,跳下一回合")
continue;
}
if(後面有){
console.log("找到了,跳下一回合")
continue;
}
console.log("沒找到")
}
ex10.js
只是概念,並無法直接跑。從這個範例我們可以看到迴圈會檢查 0 到 100,並在每一回合檢查前、中、後是否有找到,只要前夾層有找到,後面的步驟就可以跳過,中、後夾層同理。如果其中一層有找到,就會因為 continue
跳下一回合,並不會印出「沒找到」,但如果都沒找到,就會執行到最後並印出「沒找到」。
¶ 多層迴圈
從剛剛到現在我們介紹的迴圈都是只有一層,也就是只能數一樣東西,例如把所有衣服檢查一遍。但假設有個情況是,訓導主任要檢查學校每班的每個學生有沒有帶違禁品,所以他需要巡過每一個班,並且每個班的所有學生都要檢查一次,這時候就會用到多層迴圈了。
以虛擬碼表示,假設有 10 個班級,每班 40 人:
for 班級 i 從 0 到 10:
for 學生 j 從 0 到 40:
檢查 i 班 j 學生
所以多層迴圈的使用情境就是,當你的迴圈邏輯有層次性。以這個例子來說,第一層迴圈是班級,第二層迴圈是學生。每跑一回合第一層迴圈,都會執行完整的第二層迴圈。
迴圈可以不只兩層,可以有三層、四層或是更多。迴圈永遠會先把最裡面的迴圈先跑完,才會接著上一層的迴圈跑完。舉例來說:
檔案:ex11.js
console.log("ijk"); // 方便對照
for(let i = 0; i <= 2; i++){
for(let j = 0; j <= 2; j++){
for(let k = 0; k <= 2; k++){
console.log(`${i}${j}${k}`);
}
}
}
執行 ex11.js
會跑出:
$ node ex11.js
ijk
000
001
002
010
011
012
...
...
221
222
ex11.js
的迴圈有三層,從外到內分別是 i
、j
、k
,我們可以觀察到 k
的數字會先跑完一輪,才輪到 j
加一,j
加一後 k
又會再次跑完一輪。直到 j
也跑過一輪,i
才會加一。驗證了迴圈必定是從裡面輪到外面,這個順序概念非常重要。
¶ 小結
迴圈和條件判斷是程式控制的基本方式,基本上靠這兩種控制行為就可以完成任何我們想要做到的邏輯。等我們把所有 JS 的概念都學會,會對如何操作迴圈更有心得。
¶ 練習題
¶ 入門
- 如何得到 N!(N階乘),也就是 $1 \times 2 \times 3 \times … \times N$?
- 試著用迴圈印出
* *** *****
¶ 進階
- 寫一個程式可以找到兩個數字的最大公因數
- 找
1
~1000
質數。提示:- 你可能會需要用到兩層迴圈
- 你大概會需要用除的來測試某個數字 N,你只需要檢查到
Math.sqrt(N)
,就可以停止檢查
- ${\pi\over 4} = \sum_{i=0}^k {(-1)^n \over 2n+1}$,我們如何得到 $\pi$ (圓周率)?
- 使用
Math.random()
(隨機生成 0 到 1 之間的小數)函數,
產生一堆介於由(0, 0)
、(0, 1)
、(1, 0)
、(1, 1)
四個點圍成的正方形之間的點,
數一下距離圓心(0.5, 0.5)
半徑是0.5
的圓內有多少個點?
因為圓面積是 $\pi r^2$,而正方形目前面積是 $2r \times 2r$,
你可以用正方形內和圓形內的點數的比例來取得圓周率 $\pi$,請用這個方法實作一次。