循环代码

编程语言对于快速完成重复性任务非常有用,无论是多个基本计算,还是其他任何你需要完成大量类似工作的情况。在这里,我们将介绍 JavaScript 中用于处理此类需求的循环结构。

预备知识 了解 HTMLCSS 基础,熟悉前面课程中介绍的 JavaScript 基础。
学习成果
  • 了解循环的目的 — 一种代码结构,允许你多次执行非常相似的操作,而无需为每次迭代重复相同的代码。
  • 通用循环类型,如 forwhile
  • 使用 for...ofmap() 等构造迭代集合。
  • 跳出循环和继续。

循环有什么用?

循环就是一遍又一遍地做同样的事情。通常,每次循环的代码会略有不同,或者相同的代码会以不同的变量运行。

循环代码示例

假设我们想在 <canvas> 元素上绘制 100 个随机圆(按下 更新 按钮可以反复运行示例以查看不同的随机集合)

这是实现此示例的 JavaScript 代码

js
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 个圆的代码部分

js
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) 返回一个介于 0x-1 之间的整数。无论我们绘制 100 个、1000 个还是 10,000 个圆,所需的代码量都是相同的。只有一个数字需要更改。

如果我们不在这里使用循环,那么我们必须为每个要绘制的圆重复以下代码

js
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 中还有其他集合,包括 SetMap

for...of 循环

遍历集合的基本工具是 for...of 循环

js
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

for (const cat of cats) {
  console.log(cat);
}

在此示例中,for (const cat of cats) 表示

  1. 给定集合 cats,获取集合中的第一个项目。
  2. 将其赋值给变量 cat,然后运行花括号 {} 之间的代码。
  3. 获取下一个项目,并重复 (2) 直到到达集合末尾。

map() 和 filter()

JavaScript 还有更专门的集合循环,我们在此提及其中两个。

你可以使用 map() 对集合中的每个项目执行操作,并创建一个包含已更改项目的新集合

js
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() 会为数组中的每个项目调用一次该函数,并传入该项目。然后它将每个函数调用的返回值添加到新数组中,最后返回新数组。在这种情况下,我们提供的函数将项目转换为大写,因此结果数组包含我们所有的大写猫咪

js
[ "LEOPARD", "SERVAL", "JAGUAR", "TIGER", "CARACAL", "LION" ]

你可以使用 filter() 来测试集合中的每个项目,并创建一个仅包含匹配项目的新集合

js
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”开头的猫咪的数组

js
[ "Leopard", "Lion" ]

请注意,map()filter() 通常都与 函数表达式 一起使用,你将在我们的 函数 课程中学习它们。使用函数表达式,我们可以将上面的示例重写得更加紧凑

js
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

const filtered = cats.filter((cat) => cat.startsWith("L"));
console.log(filtered);
// [ "Leopard", "Lion" ]

标准 for 循环

在上面的“绘制圆圈”示例中,你没有要遍历的项集合:你实际上只是想运行相同的代码 100 次。在这种情况下,你可以使用 for 循环。它的语法如下

js
for (initializer; condition; final-expression) {
  // code to run
}

这里我们有

  1. 关键字 for,后跟一些括号。

  2. 在括号内部,我们有三个由分号分隔的项目

    1. 一个**初始化器**——这通常是一个设置为数字的变量,它会递增以计算循环运行的次数。它有时也称为**计数器变量**。
    2. 一个**条件**——这定义了循环何时应该停止。这通常是一个包含比较运算符的表达式,用于测试是否已满足退出条件。
    3. 一个**最终表达式**——每次循环完成一次完整的迭代时都会评估(或运行)它。它通常用于递增(或在某些情况下递减)计数器变量,使其更接近条件不再为 true 的点。
  3. 一些包含代码块的花括号——这段代码将在每次循环迭代时运行。

计算平方

让我们看一个真实的例子,这样我们可以更清楚地了解这些功能。

js
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++) 行分解为三个部分

  1. let i = 1:计数器变量 i1 开始。请注意,我们必须为计数器使用 let,因为我们每次循环都会重新赋值。
  2. i < 10:只要 i 小于 10 就继续循环。
  3. i++:每次循环时给 i 加一。

在循环内部,我们计算当前 i 值的平方,即 i * i。我们创建一个表示我们进行的计算和结果的字符串,并将此字符串添加到输出文本中。我们还添加 \n,以便我们添加的下一个字符串将从新行开始。所以

  1. 第一次运行时,i = 1,所以我们将添加 1 x 1 = 1
  2. 第二次运行时,i = 2,所以我们将添加 2 x 2 = 4
  3. 依此类推……
  4. i 等于 10 时,我们将停止运行循环,直接跳转到循环下面的下一段代码,在新行上打印出 Finished! 消息。

使用 for 循环遍历集合

你可以使用 for 循环来迭代集合,而不是 for...of 循环。

让我们再看看上面的 for...of 示例

js
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

for (const cat of cats) {
  console.log(cat);
}

我们可以这样重写该代码

js
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

for (let i = 0; i < cats.length; i++) {
  console.log(cats[i]);
}

在这个循环中,我们将 i0 开始,当 i 达到数组长度时停止。然后在循环内部,我们使用 i 依次访问数组中的每个项。

这工作得很好,在早期版本的 JavaScript 中,for...of 不存在,所以这是遍历数组的标准方法。然而,它提供了更多将错误引入代码的机会。例如

  • 你可能会将 i1 开始,而忘记第一个数组索引是零而不是 1。
  • 你可能会在 i <= cats.length 处停止,忘记最后一个数组索引在 length - 1 处。

由于这些原因,如果可能的话,最好使用 for...of

有时你仍然需要使用 for 循环遍历数组。例如,在下面的代码中,我们想要记录一条消息来列出我们的猫

js
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 的值

js
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> 元素用于显示结果

html
<label for="search">Search by contact name: </label>
<input id="search" type="text" />
<button>Search</button>

<p></p>

现在来看 JavaScript

js
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.";
  }
});
  1. 首先,我们有一些变量定义——我们有一个联系人信息数组,其中每个项目都是一个字符串,包含用冒号分隔的姓名和电话号码。

  2. 接下来,我们为按钮(btn)附加一个事件监听器,以便在按下它时运行一些代码来执行搜索并返回结果。

  3. 我们将文本输入框中输入的值存储在一个名为 searchName 的变量中,然后清空文本输入框并再次将其聚焦,为下一次搜索做好准备。请注意,我们还对字符串运行了 toLowerCase() 方法,以便搜索不区分大小写。

  4. 现在进入有趣的部分,for...of 循环

    1. 在循环内部,我们首先在冒号字符处分割当前联系人,并将生成的两个值存储在一个名为 splitContact 的数组中。
    2. 然后我们使用条件语句来测试 splitContact[0](联系人姓名,再次使用 toLowerCase() 转换为小写)是否等于输入的 searchName。如果是,我们会在段落中输入一个字符串来报告联系人的号码,并使用 break 结束循环。
  5. 循环结束后,我们检查是否设置了联系人,如果没有,则将段落文本设置为“Contact not found.”。

注意:你也可以在 GitHub 上查看完整的源代码(也查看其实时运行)。

使用 continue 跳过迭代

continue 语句的工作方式类似于 break,但它不会完全跳出循环,而是跳到循环的下一个迭代。我们来看另一个示例,它接受一个数字作为输入,并只返回整数(整数)的平方的数字。

HTML 基本上与上一个示例相同 — 一个简单的数字输入和一个用于输出的段落。

html
<label for="number">Enter number: </label>
<input id="number" type="number" />
<button>Generate integer squares</button>

<p>Output:</p>

JavaScript 大致相同,尽管循环本身略有不同

js
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} `;
  }
});

这是输出

  1. 在这种情况下,输入应该是一个数字 (num)。for 循环的计数器从 1 开始(因为在这种情况下我们不关心 0),一个退出条件表示当计数器大于输入 num 时循环将停止,以及一个每次将计数器加 1 的迭代器。
  2. 在循环内部,我们使用 Math.sqrt(i) 找到每个数字的平方根,然后通过测试平方根是否与它向下舍入到最接近的整数(这就是 Math.floor() 对传入的数字所做的事情)相同来检查它是否是整数。
  3. 如果平方根和向下取整的平方根不相等(!==),则表示平方根不是整数,因此我们不感兴趣。在这种情况下,我们使用 continue 语句跳到下一个循环迭代,而不将数字记录在任何地方。
  4. 如果平方根是一个整数,我们会完全跳过 if 块,因此 continue 语句不会执行;相反,我们将当前的 i 值加上一个空格连接到段落内容的末尾。

注意:你也可以在 GitHub 上查看完整的源代码(也查看其实时运行)。

while 和 do...while

for 并不是 JavaScript 中唯一可用的通用循环类型。实际上还有很多其他类型,虽然你现在不需要理解所有这些,但值得了解其中几个的结构,这样你就可以以稍微不同的方式识别相同的功能。

首先,让我们看看 while 循环。这个循环的语法看起来像这样

js
initializer
while (condition) {
  // code to run

  final-expression
}

这与 for 循环的工作方式非常相似,只是初始化器变量在循环之前设置,而最终表达式包含在循环内部的代码运行之后,而不是这两个项目包含在括号内。条件包含在括号内,括号前面是 while 关键字而不是 for

同样三个项目仍然存在,它们仍然以与 for 循环中相同的顺序定义。这是因为在检查条件是否为 true 之前,你必须定义一个初始化器。然后,在循环内部的代码运行(迭代已完成)之后运行最终表达式,这只会在条件仍然为 true 时发生。

让我们再次看看我们的猫咪列表示例,但重写为使用 while 循环

js
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 结构的一个变体

js
initializer
do {
  // code to run

  final-expression
} while (condition)

在这种情况下,初始化器再次排在第一位,在循环开始之前。关键字直接位于包含要运行的代码和最终表达式的花括号之前。

do...while 循环和 while 循环之间的主要区别在于,_do...while 循环内部的代码总是至少执行一次_。这是因为条件位于循环内部代码之后。所以我们总是先运行该代码,然后检查是否需要再次运行。在 whilefor 循环中,检查在先,因此代码可能永远不会执行。

让我们再次重写猫咪列表示例,使用 do...while 循环

js
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 倒数到“发射”打印到输出框中。

完成练习

  1. 单击下面代码块中的**“播放”**以在 MDN Playground 中编辑示例。
  2. 添加代码以从 10 循环到 0。我们已经为你提供了一个初始化器 — let i = 10;
  3. 对于每次迭代,创建一个新段落并将其追加到输出 <div> 中,我们已使用 const output = document.querySelector('.output'); 选择了它。我们已经在注释中为你提供了三行代码,需要在循环中的某个位置使用它们
    1. const para = document.createElement('p'); — 创建一个新段落。
    2. output.appendChild(para); — 将段落追加到输出 <div>
    3. para.textContent = — 使段落内的文本等于你放在等号右侧的任何内容。
  4. 对于下面列出的不同迭代次数,编写代码以在段落中插入所需的文本(你需要一个条件语句和多个 para.textContent = 行)
    1. 如果数字是 10,则将“Countdown 10”打印到段落中。
    2. 如果数字是 0,则将“Blast off!”打印到段落中。
    3. 对于任何其他数字,仅将数字打印到段落中。
  5. 记住要包含迭代器!但是,在此示例中,我们每次迭代都在倒计时,而不是向上计数,所以你**不**需要 i++ — 你如何向下迭代?

注意:如果你开始键入循环(例如 (while(i>=0)),浏览器可能会陷入无限循环,因为你尚未输入结束条件。所以要小心。你可以开始在注释中编写代码以解决此问题,并在完成后删除注释。

如果你犯了错误,可以使用 MDN Playground 中的 _重置_ 按钮清除你的工作。如果你实在卡住了,可以在实时输出下方查看解决方案。

js
const output = document.querySelector(".output");
output.textContent = "";

// let i = 10;

// const para = document.createElement('p');
// para.textContent = ;
// output.appendChild(para);
点击此处显示解决方案

你完成的 JavaScript 应该看起来像这样

js
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--;
}

填写宾客名单

在此练习中,我们希望你将存储在数组中的姓名列表放入宾客名单中。但这并非易事——我们不想让菲尔和洛拉进来,因为他们贪婪无礼,总是吃光所有食物!我们有两个名单,一个用于允许进入的宾客,一个用于拒绝进入的宾客。

完成练习

  1. 单击下面代码块中的**“播放”**以在 MDN Playground 中编辑示例。
  2. 编写一个循环,遍历 people 数组。
  3. 在每次循环迭代中,使用条件语句检查当前数组项是否等于“Phil”或“Lola”
    1. 如果是,则将数组项连接到 refused 段落的 textContent 的末尾,后跟逗号和空格。
    2. 如果不是,则将数组项连接到 admitted 段落的 textContent 的末尾,后跟逗号和空格。

我们已经为您提供了

  • refused.textContent += — 将某些内容连接到 refused.textContent 末尾的行开头。
  • admitted.textContent += — 将某些内容连接到 admitted.textContent 末尾的行开头。

额外加分问题——成功完成上述任务后,你将得到两个由逗号分隔的姓名列表,但它们会显得不整洁——每个列表的末尾都会有一个逗号。你能想出如何编写代码,在每种情况下都切掉最后一个逗号并在末尾添加一个句号吗?请查看有用的字符串方法文章以获取帮助。

如果你犯了错误,可以使用 MDN Playground 中的 _重置_ 按钮清除你的工作。如果你实在卡住了,可以在实时输出下方查看解决方案。

js
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 应该看起来像这样

js
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 是最佳选择。它更容易阅读,也更不容易出错。

对于其他用途,forwhiledo...while 循环大致可以互换使用。它们都可以用来解决相同的问题,你使用哪一个主要取决于你的个人偏好——你觉得哪一个最容易记住或最直观。我们建议使用 for,至少一开始是这样,因为它可能最容易记住所有内容——初始化器、条件和最终表达式都必须整齐地放在括号中,因此很容易看出它们在哪里并检查你是否没有遗漏它们。

让我们再看看它们。

首先是 for...of

js
for (const item of array) {
  // code to run
}

for:

js
for (initializer; condition; final-expression) {
  // code to run
}

while:

js
initializer
while (condition) {
  // code to run

  final-expression
}

最后是 do...while

js
initializer
do {
  // code to run

  final-expression
} while (condition)

注意:还有其他循环类型/功能,它们在高级/特殊情况下很有用,并且超出了本文的范围。如果你想进一步学习循环,请阅读我们的高级 循环和迭代指南

总结

本文向你揭示了 JavaScript 中循环代码背后的基本概念和可用选项。现在你应该清楚为什么循环是处理重复代码的好机制,并渴望在自己的示例中使用它们!

在下一篇文章中,我们将为你提供一些测试,你可以用它们来检查你对这些信息的理解和记忆程度。

另见