迴圈程式碼
程式語言對於快速完成重複性任務非常有用,無論是多個基本計算,還是其他任何你需要完成大量類似工作的情況。在這裡,我們將介紹 JavaScript 中用於處理此類需求的迴圈結構。
迴圈有什麼用?
迴圈就是一遍又一遍地做同樣的事情。通常,每次迴圈的程式碼會略有不同,或者相同的程式碼會以不同的變數執行。
迴圈程式碼示例
假設我們想在 <canvas> 元素上繪製 100 個隨機圓(按下 更新 按鈕可以反覆執行示例以檢視不同的隨機集合)
這是實現此示例的 JavaScript 程式碼
const btn = document.querySelector("button");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = document.documentElement.clientWidth;
canvas.height = document.documentElement.clientHeight;
function random(number) {
return Math.floor(Math.random() * number);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 100; i++) {
ctx.beginPath();
ctx.fillStyle = "rgb(255 0 0 / 50%)";
ctx.arc(
random(canvas.width),
random(canvas.height),
random(50),
0,
2 * Math.PI,
);
ctx.fill();
}
}
btn.addEventListener("click", draw);
有迴圈和無迴圈
你現在不必理解所有程式碼,但讓我們看看實際繪製 100 個圓的程式碼部分
for (let i = 0; i < 100; i++) {
ctx.beginPath();
ctx.fillStyle = "rgb(255 0 0 / 50%)";
ctx.arc(
random(canvas.width),
random(canvas.height),
random(50),
0,
2 * Math.PI,
);
ctx.fill();
}
你應該大致瞭解——我們使用迴圈執行此程式碼 100 次迭代,每次迭代都會在頁面上的隨機位置繪製一個圓。程式碼前面定義的 random(x) 返回一個介於 0 和 x-1 之間的整數。無論我們繪製 100 個、1000 個還是 10,000 個圓,所需的程式碼量都是相同的。只有一個數字需要更改。
如果我們不在這裡使用迴圈,那麼我們必須為每個要繪製的圓重複以下程式碼
ctx.beginPath();
ctx.fillStyle = "rgb(255 0 0 / 50%)";
ctx.arc(
random(canvas.width),
random(canvas.height),
random(50),
0,
2 * Math.PI,
);
ctx.fill();
這將變得非常枯燥且難以維護。
遍歷集合
大多數時候,當你使用迴圈時,你將擁有一組專案並希望對每個專案執行操作。
一種集合是 Array,我們在本課程的 陣列 一章中遇到過。但 JavaScript 中還有其他集合,包括 Set 和 Map。
for...of 迴圈
遍歷集合的基本工具是 for...of 迴圈
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
for (const cat of cats) {
console.log(cat);
}
在此示例中,for (const cat of cats) 表示
- 給定集合
cats,獲取集合中的第一個專案。 - 將其賦值給變數
cat,然後執行花括號{}之間的程式碼。 - 獲取下一個專案,並重復 (2) 直到到達集合末尾。
map() 和 filter()
JavaScript 還有更專門的集合迴圈,我們在此提及其中兩個。
你可以使用 map() 對集合中的每個專案執行操作,並建立一個包含已更改專案的新集合
function toUpper(string) {
return string.toUpperCase();
}
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
const upperCats = cats.map(toUpper);
console.log(upperCats);
// [ "LEOPARD", "SERVAL", "JAGUAR", "TIGER", "CARACAL", "LION" ]
在這裡,我們將一個函式傳遞給 cats.map(),map() 會為陣列中的每個專案呼叫一次該函式,並傳入該專案。然後它將每個函式呼叫的返回值新增到新陣列中,最後返回新陣列。在這種情況下,我們提供的函式將專案轉換為大寫,因此結果陣列包含我們所有的大寫貓咪
[ "LEOPARD", "SERVAL", "JAGUAR", "TIGER", "CARACAL", "LION" ]
你可以使用 filter() 來測試集合中的每個專案,並建立一個僅包含匹配專案的新集合
function lCat(cat) {
return cat.startsWith("L");
}
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
const filtered = cats.filter(lCat);
console.log(filtered);
// [ "Leopard", "Lion" ]
這看起來很像 map(),除了我們傳入的函式返回一個 布林值:如果它返回 true,則該專案包含在新陣列中。我們的函式測試專案是否以字母“L”開頭,因此結果是一個只包含名稱以“L”開頭的貓咪的陣列
[ "Leopard", "Lion" ]
請注意,map() 和 filter() 通常都與 函式表示式 一起使用,你將在我們的 函式 課程中學習它們。使用函式表示式,我們可以將上面的示例重寫得更加緊湊
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
const filtered = cats.filter((cat) => cat.startsWith("L"));
console.log(filtered);
// [ "Leopard", "Lion" ]
標準 for 迴圈
在上面的“繪製圓圈”示例中,你沒有要遍歷的項集合:你實際上只是想執行相同的程式碼 100 次。在這種情況下,你可以使用 for 迴圈。它的語法如下
for (initializer; condition; final-expression) {
// code to run
}
這裡我們有
-
關鍵字
for,後跟一些括號。 -
在括號內部,我們有三個由分號分隔的專案
- 一個**初始化器**——這通常是一個設定為數字的變數,它會遞增以計算迴圈執行的次數。它有時也稱為**計數器變數**。
- 一個**條件**——這定義了迴圈何時應該停止。這通常是一個包含比較運算子的表示式,用於測試是否已滿足退出條件。
- 一個**最終表示式**——每次迴圈完成一次完整的迭代時都會評估(或執行)它。它通常用於遞增(或在某些情況下遞減)計數器變數,使其更接近條件不再為
true的點。
-
一些包含程式碼塊的花括號——這段程式碼將在每次迴圈迭代時執行。
計算平方
讓我們看一個真實的例子,這樣我們可以更清楚地瞭解這些功能。
const results = document.querySelector("#results");
function calculate() {
for (let i = 1; i < 10; i++) {
const newResult = `${i} x ${i} = ${i * i}`;
results.textContent += `${newResult}\n`;
}
results.textContent += "\nFinished!\n\n";
}
const calculateBtn = document.querySelector("#calculate");
const clearBtn = document.querySelector("#clear");
calculateBtn.addEventListener("click", calculate);
clearBtn.addEventListener("click", () => (results.textContent = ""));
這給我們以下輸出
此程式碼計算從 1 到 9 的數字的平方,並輸出結果。程式碼的核心是執行計算的 for 迴圈。
讓我們將 for (let i = 1; i < 10; i++) 行分解為三個部分
let i = 1:計數器變數i從1開始。請注意,我們必須為計數器使用let,因為我們每次迴圈都會重新賦值。i < 10:只要i小於10就繼續迴圈。i++:每次迴圈時給i加一。
在迴圈內部,我們計算當前 i 值的平方,即 i * i。我們建立一個表示我們進行的計算和結果的字串,並將此字串新增到輸出文字中。我們還新增 \n,以便我們新增的下一個字串將從新行開始。所以
- 第一次執行時,
i = 1,所以我們將新增1 x 1 = 1。 - 第二次執行時,
i = 2,所以我們將新增2 x 2 = 4。 - 依此類推……
- 當
i等於10時,我們將停止執行迴圈,直接跳轉到迴圈下面的下一段程式碼,在新行上打印出Finished!訊息。
使用 for 迴圈遍歷集合
你可以使用 for 迴圈來迭代集合,而不是 for...of 迴圈。
讓我們再看看上面的 for...of 示例
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
for (const cat of cats) {
console.log(cat);
}
我們可以這樣重寫該程式碼
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];
for (let i = 0; i < cats.length; i++) {
console.log(cats[i]);
}
在這個迴圈中,我們將 i 從 0 開始,當 i 達到陣列長度時停止。然後在迴圈內部,我們使用 i 依次訪問陣列中的每個項。
這工作得很好,在早期版本的 JavaScript 中,for...of 不存在,所以這是遍歷陣列的標準方法。然而,它提供了更多將錯誤引入程式碼的機會。例如
- 你可能會將
i從1開始,而忘記第一個陣列索引是零而不是 1。 - 你可能會在
i <= cats.length處停止,忘記最後一個數組索引在length - 1處。
由於這些原因,如果可能的話,最好使用 for...of。
有時你仍然需要使用 for 迴圈遍歷陣列。例如,在下面的程式碼中,我們想要記錄一條訊息來列出我們的貓
const cats = ["Pete", "Biggles", "Jasmine"];
let myFavoriteCats = "My cats are called ";
for (const cat of cats) {
myFavoriteCats += `${cat}, `;
}
console.log(myFavoriteCats); // "My cats are called Pete, Biggles, Jasmine, "
最終的輸出句子格式不太好
My cats are called Pete, Biggles, Jasmine,
我們希望它能以不同的方式處理最後一隻貓,就像這樣
My cats are called Pete, Biggles, and Jasmine.
但是要做到這一點,我們需要知道何時處於最終迴圈迭代,為此我們可以使用 for 迴圈並檢查 i 的值
const cats = ["Pete", "Biggles", "Jasmine"];
let myFavoriteCats = "My cats are called ";
for (let i = 0; i < cats.length; i++) {
if (i === cats.length - 1) {
// We are at the end of the array
myFavoriteCats += `and ${cats[i]}.`;
} else {
myFavoriteCats += `${cats[i]}, `;
}
}
console.log(myFavoriteCats); // "My cats are called Pete, Biggles, and Jasmine."
使用 break 退出迴圈
如果你想在所有迭代完成之前退出迴圈,可以使用 break 語句。我們在上一篇文章中討論 switch 語句 時已經遇到過它——當 switch 語句中與輸入表示式匹配的 case 滿足時,break 語句會立即退出 switch 語句並繼續執行其後面的程式碼。
迴圈也是如此——一個 break 語句將立即退出迴圈,並讓瀏覽器繼續執行其後面的任何程式碼。
假設我們想要搜尋一個包含聯絡人和電話號碼的陣列,並只返回我們想要查詢的號碼?首先,一些簡單的 HTML——一個文字 <input> 允許我們輸入要搜尋的名稱,一個 <button> 元素用於提交搜尋,以及一個 <p> 元素用於顯示結果
<label for="search">Search by contact name: </label>
<input id="search" type="text" />
<button>Search</button>
<p></p>
現在來看 JavaScript
const contacts = [
"Chris:2232322",
"Sarah:3453456",
"Bill:7654322",
"Mary:9998769",
"Dianne:9384975",
];
const para = document.querySelector("p");
const input = document.querySelector("input");
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
const searchName = input.value.toLowerCase();
input.value = "";
input.focus();
para.textContent = "";
for (const contact of contacts) {
const splitContact = contact.split(":");
if (splitContact[0].toLowerCase() === searchName) {
para.textContent = `${splitContact[0]}'s number is ${splitContact[1]}.`;
break;
}
}
if (para.textContent === "") {
para.textContent = "Contact not found.";
}
});
-
首先,我們有一些變數定義——我們有一個聯絡人資訊陣列,其中每個專案都是一個字串,包含用冒號分隔的姓名和電話號碼。
-
接下來,我們為按鈕(
btn)附加一個事件監聽器,以便在按下它時執行一些程式碼來執行搜尋並返回結果。 -
我們將文字輸入框中輸入的值儲存在一個名為
searchName的變數中,然後清空文字輸入框並再次將其聚焦,為下一次搜尋做好準備。請注意,我們還對字串運行了toLowerCase()方法,以便搜尋不區分大小寫。 -
現在進入有趣的部分,
for...of迴圈- 在迴圈內部,我們首先在冒號字元處分割當前聯絡人,並將生成的兩個值儲存在一個名為
splitContact的陣列中。 - 然後我們使用條件語句來測試
splitContact[0](聯絡人姓名,再次使用toLowerCase()轉換為小寫)是否等於輸入的searchName。如果是,我們會在段落中輸入一個字串來報告聯絡人的號碼,並使用break結束迴圈。
- 在迴圈內部,我們首先在冒號字元處分割當前聯絡人,並將生成的兩個值儲存在一個名為
-
迴圈結束後,我們檢查是否設定了聯絡人,如果沒有,則將段落文字設定為“Contact not found.”。
注意:你也可以在 GitHub 上檢視完整的原始碼(也檢視其即時執行)。
使用 continue 跳過迭代
continue 語句的工作方式類似於 break,但它不會完全跳出迴圈,而是跳到迴圈的下一個迭代。我們來看另一個示例,它接受一個數字作為輸入,並只返回整數(整數)的平方的數字。
HTML 基本上與上一個示例相同 — 一個簡單的數字輸入和一個用於輸出的段落。
<label for="number">Enter number: </label>
<input id="number" type="number" />
<button>Generate integer squares</button>
<p>Output:</p>
JavaScript 大致相同,儘管迴圈本身略有不同
const para = document.querySelector("p");
const input = document.querySelector("input");
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
para.textContent = "Output: ";
const num = input.value;
input.value = "";
input.focus();
for (let i = 1; i <= num; i++) {
let sqRoot = Math.sqrt(i);
if (Math.floor(sqRoot) !== sqRoot) {
continue;
}
para.textContent += `${i} `;
}
});
這是輸出
- 在這種情況下,輸入應該是一個數字 (
num)。for迴圈的計數器從 1 開始(因為在這種情況下我們不關心 0),一個退出條件表示當計數器大於輸入num時迴圈將停止,以及一個每次將計數器加 1 的迭代器。 - 在迴圈內部,我們使用
Math.sqrt(i)找到每個數字的平方根,然後透過測試平方根是否與它向下舍入到最接近的整數(這就是Math.floor()對傳入的數字所做的事情)相同來檢查它是否是整數。 - 如果平方根和向下取整的平方根不相等(
!==),則表示平方根不是整數,因此我們不感興趣。在這種情況下,我們使用continue語句跳到下一個迴圈迭代,而不將數字記錄在任何地方。 - 如果平方根是一個整數,我們會完全跳過
if塊,因此continue語句不會執行;相反,我們將當前的i值加上一個空格連線到段落內容的末尾。
注意:你也可以在 GitHub 上檢視完整的原始碼(也檢視其即時執行)。
while 和 do...while
for 並不是 JavaScript 中唯一可用的通用迴圈型別。實際上還有很多其他型別,雖然你現在不需要理解所有這些,但值得了解其中幾個的結構,這樣你就可以以稍微不同的方式識別相同的功能。
首先,讓我們看看 while 迴圈。這個迴圈的語法看起來像這樣
initializer
while (condition) {
// code to run
final-expression
}
這與 for 迴圈的工作方式非常相似,只是初始化器變數在迴圈之前設定,而最終表示式包含在迴圈內部的程式碼執行之後,而不是這兩個專案包含在括號內。條件包含在括號內,括號前面是 while 關鍵字而不是 for。
同樣三個專案仍然存在,它們仍然以與 for 迴圈中相同的順序定義。這是因為在檢查條件是否為 true 之前,你必須定義一個初始化器。然後,在迴圈內部的程式碼執行(迭代已完成)之後執行最終表示式,這隻會在條件仍然為 true 時發生。
讓我們再次看看我們的貓咪列表示例,但重寫為使用 while 迴圈
const cats = ["Pete", "Biggles", "Jasmine"];
let myFavoriteCats = "My cats are called ";
let i = 0;
while (i < cats.length) {
if (i === cats.length - 1) {
myFavoriteCats += `and ${cats[i]}.`;
} else {
myFavoriteCats += `${cats[i]}, `;
}
i++;
}
console.log(myFavoriteCats); // "My cats are called Pete, Biggles, and Jasmine."
注意:這仍然按預期工作——請在 GitHub 上檢視其即時執行(也可以檢視完整的原始碼)。
do...while 迴圈非常相似,但提供了 while 結構的一個變體
initializer
do {
// code to run
final-expression
} while (condition)
在這種情況下,初始化器再次排在第一位,在迴圈開始之前。關鍵字直接位於包含要執行的程式碼和最終表示式的花括號之前。
do...while 迴圈和 while 迴圈之間的主要區別在於,_do...while 迴圈內部的程式碼總是至少執行一次_。這是因為條件位於迴圈內部程式碼之後。所以我們總是先執行該程式碼,然後檢查是否需要再次執行。在 while 和 for 迴圈中,檢查在先,因此程式碼可能永遠不會執行。
讓我們再次重寫貓咪列表示例,使用 do...while 迴圈
const cats = ["Pete", "Biggles", "Jasmine"];
let myFavoriteCats = "My cats are called ";
let i = 0;
do {
if (i === cats.length - 1) {
myFavoriteCats += `and ${cats[i]}.`;
} else {
myFavoriteCats += `${cats[i]}, `;
}
i++;
} while (i < cats.length);
console.log(myFavoriteCats); // "My cats are called Pete, Biggles, and Jasmine."
注意:同樣,這仍然按預期工作——請在 GitHub 上檢視其即時執行(也可以檢視完整的原始碼)。
警告:對於任何型別的迴圈,你必須確保初始化器遞增或(根據情況)遞減,以便條件最終變為假。否則,迴圈將永遠進行下去,瀏覽器會強制其停止,或者它會崩潰。這稱為**無限迴圈**。
實現發射倒計時
在此練習中,我們希望你將一個簡單的發射倒計時從 10 倒數到“發射”列印到輸出框中。
完成練習
- 單擊下面程式碼塊中的**“播放”**以在 MDN Playground 中編輯示例。
- 新增程式碼以從 10 迴圈到 0。我們已經為你提供了一個初始化器 —
let i = 10;。 - 對於每次迭代,建立一個新段落並將其追加到輸出
<div>中,我們已使用const output = document.querySelector('.output');選擇了它。我們已經在註釋中為你提供了三行程式碼,需要在迴圈中的某個位置使用它們const para = document.createElement('p');— 建立一個新段落。output.appendChild(para);— 將段落追加到輸出<div>。para.textContent =— 使段落內的文字等於你放在等號右側的任何內容。
- 對於下面列出的不同迭代次數,編寫程式碼以在段落中插入所需的文字(你需要一個條件語句和多個
para.textContent =行)- 如果數字是 10,則將“Countdown 10”列印到段落中。
- 如果數字是 0,則將“Blast off!”列印到段落中。
- 對於任何其他數字,僅將數字列印到段落中。
- 記住要包含迭代器!但是,在此示例中,我們每次迭代都在倒計時,而不是向上計數,所以你**不**需要
i++— 你如何向下迭代?
注意:如果你開始鍵入迴圈(例如 (while(i>=0)),瀏覽器可能會陷入無限迴圈,因為你尚未輸入結束條件。所以要小心。你可以開始在註釋中編寫程式碼以解決此問題,並在完成後刪除註釋。
如果你犯了錯誤,可以使用 MDN Playground 中的 _重置_ 按鈕清除你的工作。如果你實在卡住了,可以在即時輸出下方檢視解決方案。
const output = document.querySelector(".output");
output.textContent = "";
// let i = 10;
// const para = document.createElement('p');
// para.textContent = ;
// output.appendChild(para);
點選此處顯示解決方案
你完成的 JavaScript 應該看起來像這樣
const output = document.querySelector(".output");
output.textContent = "";
let i = 10;
while (i >= 0) {
const para = document.createElement("p");
if (i === 10) {
para.textContent = `Countdown ${i}`;
} else if (i === 0) {
para.textContent = "Blast off!";
} else {
para.textContent = i;
}
output.appendChild(para);
i--;
}
填寫賓客名單
在此練習中,我們希望你將儲存在陣列中的姓名列表放入賓客名單中。但這並非易事——我們不想讓菲爾和洛拉進來,因為他們貪婪無禮,總是吃光所有食物!我們有兩個名單,一個用於允許進入的賓客,一個用於拒絕進入的賓客。
完成練習
- 單擊下面程式碼塊中的**“播放”**以在 MDN Playground 中編輯示例。
- 編寫一個迴圈,遍歷
people陣列。 - 在每次迴圈迭代中,使用條件語句檢查當前陣列項是否等於“Phil”或“Lola”
- 如果是,則將陣列項連線到
refused段落的textContent的末尾,後跟逗號和空格。 - 如果不是,則將陣列項連線到
admitted段落的textContent的末尾,後跟逗號和空格。
- 如果是,則將陣列項連線到
我們已經為您提供了
refused.textContent +=— 將某些內容連線到refused.textContent末尾的行開頭。admitted.textContent +=— 將某些內容連線到admitted.textContent末尾的行開頭。
額外加分問題——成功完成上述任務後,你將得到兩個由逗號分隔的姓名列表,但它們會顯得不整潔——每個列表的末尾都會有一個逗號。你能想出如何編寫程式碼,在每種情況下都切掉最後一個逗號並在末尾新增一個句號嗎?請檢視有用的字串方法文章以獲取幫助。
如果你犯了錯誤,可以使用 MDN Playground 中的 _重置_ 按鈕清除你的工作。如果你實在卡住了,可以在即時輸出下方檢視解決方案。
const people = [
"Chris",
"Anne",
"Colin",
"Terri",
"Phil",
"Lola",
"Sam",
"Kay",
"Bruce",
];
const admitted = document.querySelector(".admitted");
const refused = document.querySelector(".refused");
admitted.textContent = "Admit: ";
refused.textContent = "Refuse: ";
// loop starts here
// refused.textContent += ...;
// admitted.textContent += ...;
點選此處顯示解決方案
你完成的 JavaScript 應該看起來像這樣
const people = [
"Chris",
"Anne",
"Colin",
"Terri",
"Phil",
"Lola",
"Sam",
"Kay",
"Bruce",
];
const admitted = document.querySelector(".admitted");
const refused = document.querySelector(".refused");
admitted.textContent = "Admit: ";
refused.textContent = "Refuse: ";
for (const person of people) {
if (person === "Phil" || person === "Lola") {
refused.textContent += `${person}, `;
} else {
admitted.textContent += `${person}, `;
}
}
refused.textContent = `${refused.textContent.slice(0, -2)}.`;
admitted.textContent = `${admitted.textContent.slice(0, -2)}.`;
應該使用哪種迴圈型別?
如果你正在迭代陣列或其他支援它的物件,並且不需要訪問每個項的索引位置,那麼 for...of 是最佳選擇。它更容易閱讀,也更不容易出錯。
對於其他用途,for、while 和 do...while 迴圈大致可以互換使用。它們都可以用來解決相同的問題,你使用哪一個主要取決於你的個人偏好——你覺得哪一個最容易記住或最直觀。我們建議使用 for,至少一開始是這樣,因為它可能最容易記住所有內容——初始化器、條件和最終表示式都必須整齊地放在括號中,因此很容易看出它們在哪裡並檢查你是否沒有遺漏它們。
讓我們再看看它們。
首先是 for...of
for (const item of array) {
// code to run
}
for:
for (initializer; condition; final-expression) {
// code to run
}
while:
initializer
while (condition) {
// code to run
final-expression
}
最後是 do...while
initializer
do {
// code to run
final-expression
} while (condition)
注意:還有其他迴圈型別/功能,它們在高階/特殊情況下很有用,並且超出了本文的範圍。如果你想進一步學習迴圈,請閱讀我們的高階 迴圈和迭代指南。
總結
本文向你揭示了 JavaScript 中迴圈程式碼背後的基本概念和可用選項。現在你應該清楚為什麼迴圈是處理重複程式碼的好機制,並渴望在自己的示例中使用它們!
在下一篇文章中,我們將為你提供一些測試,你可以用它們來檢查你對這些資訊的理解和記憶程度。
另見
- 迴圈和迭代詳情
- for...of 參考
- for 語句參考
- while 和 do...while 參考
- break 和 continue 參考