循环代码

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

先决条件 对 HTML、CSS 和 JavaScript 初步知识 的基本了解。
目标 了解如何在 JavaScript 中使用循环。

为什么循环有用?

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

循环代码示例

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

以下是实现此示例的 JavaScript 代码

js
const btn = document.querySelector("button");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

document.addEventListener("DOMContentLoaded", () => {
  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();
}
  • random(x)(在代码的前面定义)返回 0x-1 之间的一个整数。

您应该掌握基本思路——我们使用循环运行此代码的 100 次迭代,每次迭代都在页面上的随机位置绘制一个圆圈。无论我们绘制 100 个、1000 个还是 10000 个圆圈,所需的代码量都相同。只需要更改一个数字。

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

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 加 1。

在循环内部,我们计算 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]);
}

在此循环中,我们从 0 开始 i,并在 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. 循环结束后,我们检查是否设置了联系人,如果没有,我们将段落文本设置为“未找到联系人”。

注意:您也可以在 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 循环中相同。这是因为您必须在检查条件是否为真之前定义初始化器。然后在循环内部的代码运行后(一个迭代已完成)运行最终表达式,这只有在条件仍然为真的情况下才会发生。

让我们再看一下我们的猫咪列表示例,但重写为使用 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 上的实时运行效果(还可以查看 完整源代码)。

警告:对于 whiledo...while——以及所有循环——您必须确保初始化器递增或根据情况递减,以便条件最终变为假。否则,循环将永远持续下去,浏览器要么强制其停止,要么会崩溃。这称为无限循环

主动学习:启动倒计时

在本练习中,我们希望您将一个简单的发射倒计时打印到输出框中,从 10 倒数到发射。具体来说,我们希望您:

  • 从 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,则将“倒计时 10”打印到段落中。
    • 如果数字是 0,则将“发射!”打印到段落中。
    • 对于任何其他数字,只需将数字打印到段落中。
  • 请记住包含迭代器!但是,在本例中,我们在每次迭代后递减计数,而不是递增,因此您不需要 i++——您如何向下迭代?

注意:如果您开始键入循环(例如 while(i>=0)),浏览器可能会卡住,因为您尚未输入结束条件。因此,请注意这一点。您可以先在注释中编写代码以解决此问题,并在完成后删除注释。

如果您犯了错误,您可以随时使用“重置”按钮重置示例。如果您真的卡住了,请按“显示解决方案”以查看解决方案。

css
html {
  font-family: sans-serif;
}

h2 {
  font-size: 16px;
}

.a11y-label {
  margin: 0;
  text-align: right;
  font-size: 0.7rem;
  width: 98%;
}

body {
  margin: 10px;
  background: #f5f9fa;
}

主动学习:填写宾客名单

在本练习中,我们希望您获取存储在数组中的姓名列表,并将它们放入访客列表中。但这并不容易——我们不想让 Phil 和 Lola 进来,因为他们贪婪且粗鲁,总是吃掉所有食物!我们有两个列表,一个用于允许的访客,一个用于拒绝的访客。

具体来说,我们希望您:

  • 编写一个循环,遍历 people 数组。
  • 在每次循环迭代期间,使用条件语句检查当前数组项是否等于“Phil”或“Lola”。
    • 如果是,则将数组项连接到 refused 段落的 textContent 末尾,后跟逗号和空格。
    • 如果不是,则将数组项连接到 admitted 段落的 textContent 末尾,后跟逗号和空格。

我们已经为您提供了:

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

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

如果您犯了错误,您可以随时使用“重置”按钮重置示例。如果您真的卡住了,请按“显示解决方案”以查看解决方案。

应该使用哪种循环类型?

如果您正在遍历一个数组或其他支持它的对象,并且不需要访问每个项目的索引位置,那么 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 中循环代码背后的基本概念以及可用的不同选项。您现在应该清楚为什么循环是处理重复代码的良好机制,并且渴望在您自己的示例中使用它们!

如果您有任何不理解的地方,请随时重新阅读本文,或 联系我们 寻求帮助。

另请参阅