循环代码
编程语言对于快速完成重复性任务非常有用,无论是多个基本计算,还是其他任何你需要完成大量类似工作的情况。在这里,我们将介绍 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 参考