迴圈

在生活中,我們常常會有有做一樣的事情的時候,例如一張一張比對發票、一行一行檢查有沒有錯字。這個要重複做的動作,在程式語言中就可以用迴圈(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),與 ifelse 雷同,如同前一章所介紹。如果變數是在某個區塊 { } 裡面定義,變數的生命只在那個區塊 { } 的區間裡面存在,一但出了 { } 就會被銷毀。在迴圈中,每一回合就是一個 { },下一回合等同離開上一回合的 { }

所以假設我們這樣寫:

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 語法。兩個概念是一樣的,都是會在條件內反覆執行,不過使用方法有點不同,接下來我們比對 forwhile 兩者即能看出差別。

舉個例子,舉個例子我們先用 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.jsex5-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() 答案,然後把自己記錄成答案。

接著迴圈會一直跑,直到某一個回合 resultfoo() 函數得到的結果是結束了,這時候 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 是一模一樣的,所以其實同樣的邏輯 whilefor 都辦得到,要用哪個端看使用者的心情。

不過你知道 ex7.js 最後的 i 會是多少嗎?若是我們在最後一行之後,再加上 console.log(i); 會印出多少數字呢?可以實驗看看並想想為什麼。

迴圈操作

再來要介紹迴圈中兩種特別的指令 break & continuebreak 是可以直接中斷迴圈的指令,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 的迴圈有三層,從外到內分別是 ijk,我們可以觀察到 k 的數字會先跑完一輪,才輪到 j 加一,j 加一後 k 又會再次跑完一輪。直到 j 也跑過一輪,i 才會加一。驗證了迴圈必定是從裡面輪到外面,這個順序概念非常重要。

小結

迴圈和條件判斷是程式控制的基本方式,基本上靠這兩種控制行為就可以完成任何我們想要做到的邏輯。等我們把所有 JS 的概念都學會,會對如何操作迴圈更有心得。

練習題

入門

  1. 如何得到 N!(N階乘),也就是 $1 \times 2 \times 3 \times … \times N$?
  2. 試著用迴圈印出
      *
     ***
    *****
    

進階

  1. 寫一個程式可以找到兩個數字的最大公因數
  2. 1 ~ 1000 質數。提示:
    1. 你可能會需要用到兩層迴圈
    2. 你大概會需要用除的來測試某個數字 N,你只需要檢查到 Math.sqrt(N),就可以停止檢查
  3. ${\pi\over 4} = \sum_{i=0}^k {(-1)^n \over 2n+1}$,我們如何得到 $\pi$ (圓周率)?
  4. 使用 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$,請用這個方法實作一次。