Skip to content
This page has been auto-translated and may contain errors.View in English

循环

docs.scrimba.com

假设你想打印数字 1 到 100,或者问候一个单词中的每个字符,或者持续提问一个问题直到答案正确。你可以写一百次同样的行,但这很繁琐,而且改起来更麻烦。循环是告诉 JavaScript 一遍又一遍地运行一段代码的方式,所以你只需写一次工作代码,让语言来完成重复。本章介绍你最常用的三个循环:forwhilefor...of

for 循环

**for 循环**是当你知道要重复多少次时使用的。它为你计数。这里是打印数字 0 到 4 的例子:

js
for (let i = 0; i < 5; i++) {
  console.log(i);
}
// 0
// 1
// 2
// 3
// 4

括号中的部分有三个部分,由分号分隔:

  • let i = 0开始:它创建一个名为 i 的计数器,并将其设为 0。
  • i < 5条件:只要这为真,循环就继续。
  • i++步长:每次循环后,它给 i 加 1(i++i = i + 1 的简写)。

所以循环从 i 为 0 开始,检查 i < 5,运行大括号中的代码,然后加 1 再检查。当 i 到达 5 时,条件为假,循环停止。

**for 循环**适用于你预先知道重复次数的情况。它在循环头中压缩了三个部分,按顺序运行:

js
for (let i = 0; i < 5; i++) {
  console.log(`pass number ${i}`);
}
// pass number 0
// pass number 1
// pass number 2
// pass number 3
// pass number 4

从左到右读循环头:let i = 0初始化器,在任何东西之前运行一次。i < 5条件,在每次循环前检查,循环只在它为真时运行。i++步长,在每次循环后运行。计数器 i 是一个普通变量,所以你可以在循环内读取它,这就是为什么循环和计数总是结合在一起的。

按惯例,计数器从 0 开始,而不是必须这样。从 0 开始计数是你在 JavaScript 几乎到处都能看到的,它与数组如何对其项进行编号相一致,所以值得早点习惯。

**for 循环**在一个循环头中编码了整个有计数重复的形状:初始化器、条件和步长,每个由分号分隔。

js
for (let i = 0; i < 5; i++) {
  console.log(`pass number ${i}`);
}
// pass number 0
// pass number 1
// pass number 2
// pass number 3
// pass number 4

顺序是精确的,值得内化:初始化器运行一次,然后测试条件,只有当它为真时循环体才运行,然后是步长,再回到条件。这个顺序就是为什么条件在第一次循环前被检查,所以如果条件开始时为假,循环永远不会运行其循环体。

对计数器使用 let 而不是 const,因为步长在每次循环中重新赋值。在循环头中声明 i 会将其作用域限制在循环内,所以它不会泄漏到周围代码中,这防止了嵌套循环或连续编写多个循环时计数器冲突。从 0 而不是 1 开始计数是这里几乎通用的惯例,它直接映射到数组如何索引其元素的方式,所以这个习惯从你开始循环数据的那一刻起就会回报。

Junofor 循环for 循环重复固定次数并为你计数。其循环头有三个部分:开始部分如 let i = 0、保持循环运行的条件如 i < 5,以及推动循环的步长如 i++。当条件变为假时,循环停止。
Junofor 循环 当你预先知道计数时选择 for。循环头运行初始化器一次,在每次循环前检查条件,在每次循环后运行步长。从 0 开始计数是惯例,它与数组对其项进行编号的方式相一致,所以现在就养成这个习惯。
Junofor 循环 循环头顺序是固定的:初始化一次、测试条件、运行循环体、运行步长、重复。这就是为什么开始时为假的条件会完全跳过循环体。对计数器使用 let 以便步长可以重新赋值,并在循环头中声明它以使其作用域限制在循环内。

while 循环

有时你不知道要重复多少次,你只知道一个应该保持真的条件。这就是**while 循环**的用途。只要其条件成立,它就继续运行:

js
let count = 3;
while (count > 0) {
  console.log(count);
  count = count - 1;
}
console.log("Go");
// 3
// 2
// 1
// Go

循环检查 count > 0,运行代码,然后再次检查。注意循环内的某些东西改变了 count,所以条件最终变为假,循环停止。如果循环内没有任何东西使条件变为假,循环会永远运行。这是无限循环,它会冻结你的程序,所以总是确保条件能够改变。

**while 循环**只要条件保持真就重复,它没有内置的计数器或步长。这使得当重复次数不能提前知道时它是正确的工具,而当已知时 for 循环是正确的工具。

js
let attempts = 0;
let password = "";
while (password !== "letmein") {
  password = "letmein"; // 代替真实提示的占位符
  attempts = attempts + 1;
}
console.log(`Unlocked after ${attempts} attempt(s)`);
// Unlocked after 1 attempt(s)

条件(password !== "letmein")依赖于条件语句中的比较运算符,循环运行直到它变为假。问题是没有什么强制它变为假:你必须改变循环内条件依赖的某些东西。条件永不变为假的 while 循环会永远运行。这是无限循环,它会挂起程序。

**while 循环**是通用形式:它在条件为真时重复,计数(如果有的话)完全由你决定。当迭代次数不是固定的时,选择它,例如重复直到输入验证或值越过阈值。当计数已知时,for 循环更清楚地说明了意图。

js
let remaining = 100;
let steps = 0;
while (remaining > 0) {
  remaining = remaining - 15;
  steps = steps + 1;
}
console.log(`Emptied in ${steps} steps`);
// Emptied in 7 steps

要尊重的失败模式是无限循环:while 对终止没有结构保证,所以循环体必须将条件推向假。三件事导致循环挂起:条件永不更新、更新朝错误方向移动、以及条件可能被跳过而不是满足(上面使用 remaining === 0 会错过,因为 remaining 落在负数上,而不是恰好 0)。每个 while 循环都需要一行代码将其条件推向假。倾向于使用范围测试如 > 0 而不是精确的 === 0,这样超过步长的步仍然会结束循环。

Junowhile 循环while 循环只要其条件为真就重复,没有内置计数器。当你不预先知道重复次数时使用它。总是在循环内改变某些东西以便条件能变为假,否则你会得到冻结程序的无限循环。
Junowhile 循环 当重复次数未知时选择 while,当次数固定时选择 for。循环体必须改变条件测试的任何东西,否则它永不变为假。那就是无限循环,这是每次编写 while 时要注意的错误。
Junowhile 循环while 没有保证它会停止,所以循环体必须将条件推向假。三种挂起的方式:条件永不更新、它向错误方向移动、或被跳过。倾向于范围测试如 > 0 而不是精确的 === 0,以便超过的步仍然结束循环。

用 for...of 循环遍历字符串

forwhile 循环用数字计数,但通常你想直接访问某物中的每一项。for...of 就是做这个的。这里它遍历一个单词的每个字符:

js
for (const char of "hello") {
  console.log(char);
}
// h
// e
// l
// l
// o

读作"对单词的每个字符"。在每次循环中,char 保存下一个字符,你永远不接触计数器或条件。当你只想依次查看每一项时,它比 for 循环更干净。

你会主要在数组上使用 for...of,数组是值的列表,你会在接下来的章节中遇到。字符串是循环的一个很好的第一个对象,因为它已经是字符的序列,for...of 完全按那种方式对待它。

for...of 直接迭代序列的项,逐个给你每个值,无需管理计数器或条件:

js
let vowels = 0;
for (const char of "javascript") {
  if (char === "a" || char === "i") {
    vowels = vowels + 1;
  }
}
console.log(`Found ${vowels} of those vowels`);
// Found 3 of those vowels

这里使用 const char 是故意的:每次循环创建一个新的 char,你只读取它,从不重新赋值,所以 const 是合适的并表明了意图。for...of 循环用手动记账的计数循环交换可读性,它消除了整个由于手工索引导致的差一错误的类别。

字符串是循环的一个自然的第一个序列,但真正的主力是数组。一旦你接触数组,for...of 是遍历其内容的干净、直接的方式。

for...of 逐个元素地迭代任何序列,将每个值绑定到一个名字,而不暴露索引。在字符串上,它产生字符:

js
let shout = "";
for (const char of "loops") {
  shout = shout + char.toUpperCase();
}
console.log(shout);
// LOOPS

const 绑定循环变量:for...of 在每次循环中创建一个新的绑定,所以没有什么需要重新赋值,const 记录了该值在循环体内是只读的。相比计数循环的好处是你永不写索引,这按构造回避了差一错误。

这最重要的地方是数组。对于访问每个元素,for...of 比计数 for 循环读得更干净,并直接说明了意图。但当你将一个集合转换为新的集合时,数组迭代方法通常完全替代手动循环,它们在自己的章节中介绍。

break 和 continue

两个关键字在任何循环内给你更精细的控制。break 立即停止循环并移到它后面的代码。continue 跳过当前循环的其余部分,直接跳到下一次循环。

js
for (let i = 0; i < 10; i++) {
  if (i === 3) {
    continue; // 跳过 3,继续循环
  }
  if (i === 6) {
    break; // 完全停止循环
  }
  console.log(i);
}
// 0
// 1
// 2
// 4
// 5

break 是当你找到你需要的东西时提前停止的方式,所以你不会继续无谓地扫描。continue 是当你不关心某些情况时跳过它们的方式,不需要将循环体的其余部分嵌套在 if 内。

差一错误和索引边界

经典的循环 bug 是差一错误:运行一次太多或太少。它来自计数器的边界。长度为 5 的字符串在位置 0、1、2、3 和 4 上有字符,所以最后有效位置是 length - 1,而不是 length。这就是为什么对其位置的计数循环使用 i < text.length 而不是 i <= text.length

js
const text = "hello";
for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// h
// e
// l
// l
// o

i <= text.length 最后一次循环会到达位置 5,它不存在,所以它产生 undefined。从 0 开始用 < 计数到长度是恰好到达每个位置的模式,不多不少。当你只需要每一项而不需要其位置时,for...of 完全避免了边界问题,这是倾向于它的一个原因。

Juno用 for...of 循环遍历字符串for...of 直接访问每一项,没有计数器也没有条件要管理。在字符串上,每次循环给你下一个字符。你会主要在数组上使用它,但字符串是循环的一个干净的第一个对象,因为它已经是字符的序列。
Juno用 for...of 循环遍历字符串for...of 逐个给你每个值,所以你跳过了计数器和手工索引带来的差一风险。用 const 绑定,因为每次循环是只读的。字符串是一个很好的开始,但数组是你一旦接触它就会依赖的。
Juno用 for...of 循环遍历字符串for...of 将每个元素绑定到一个新的 const 并永不暴露索引,这按构造消除了差一 bug。break 提前停止,continue 跳过一次循环。对于转换集合而不仅仅是遍历它,数组迭代方法通常替代手动循环,它们有自己的章节。

循环从这里走向何处

循环是让你停止重复自己,让语言来做重复的方式。for 循环计数已知的次数,while 只要条件成立就重复,for...of 直接遍历序列的项。从这里带出的一个习惯:确保每个循环都能结束,当你只需要每一项时倾向于 for...of

循环真正重要的原因是你处理的大多数数据都以集合的形式出现。接下来的章节介绍数组,你会不断循环遍历的列表,以及稍后的数组方法,当你转换数据时它们通常替代手写的循环。如果你想加强循环依赖的条件,条件语句介绍了决定循环何时继续以及何时停止的比较。