2022 年 6 月 19 日

循环:while 和 for

我们经常需要重复操作。

例如,逐个输出列表中的商品或仅对 1 到 10 的每个数字运行相同的代码。

循环是一种多次重复相同代码的方法。

for…of 和 for…in 循环

高级读者的小公告。

本文仅介绍基本循环:whiledo..whilefor(..;..;..)

如果您来到此文章寻找其他类型的循环,这里有指针

否则,请继续阅读。

“while” 循环

while 循环具有以下语法

while (condition) {
  // code
  // so-called "loop body"
}

condition 为真时,将执行循环体中的 code

例如,下面的循环在 i < 3 时输出 i

let i = 0;
while (i < 3) { // shows 0, then 1, then 2
  alert( i );
  i++;
}

循环体的单次执行称为一次迭代。上面的示例中的循环进行三次迭代。

如果上面的示例中缺少 i++,则循环将(理论上)永远重复。在实践中,浏览器提供了停止此类循环的方法,而在服务器端 JavaScript 中,我们可以终止进程。

任何表达式或变量都可以作为循环条件,而不仅仅是比较:条件由 while 评估并转换为布尔值。

例如,编写 while (i != 0) 的较短方法是 while (i)

let i = 3;
while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops
  alert( i );
  i--;
}
单行主体不需要大括号

如果循环体只有一个语句,我们可以省略大括号 {…}

let i = 3;
while (i) alert(i--);

“do…while” 循环

可以使用 do..while 语法将条件检查移动到循环体下方

do {
  // loop body
} while (condition);

循环将首先执行主体,然后检查条件,并且在为真时一遍又一遍地执行它。

例如

let i = 0;
do {
  alert( i );
  i++;
} while (i < 3);

此语法形式仅应在您希望循环体执行至少一次而不管条件是否为真时使用。通常,首选另一种形式:while(…) {…}

“for” 循环

for 循环更复杂,但它也是最常用的循环。

它看起来像这样

for (begin; condition; step) {
  // ... loop body ...
}

让我们通过示例了解这些部分的含义。下面的循环为 i0 到(但不包括)3 运行 alert(i)

for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2
  alert(i);
}

让我们逐部分检查 for 语句

部分
开始 let i = 0 进入循环时执行一次。
条件 i < 3 在每次循环迭代之前检查。如果为假,则循环停止。
主体 alert(i) 在条件为真时一遍又一遍地运行。
步骤 i++ 在每次迭代中主体之后执行。

通用循环算法的工作原理如下

Run begin
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ ...

即,begin 执行一次,然后它进行迭代:在每次 condition 测试后,执行 bodystep

如果您是循环的新手,可以返回示例并逐步在纸上重现其运行方式,这可能会有所帮助。

以下是我们在这种情况下发生的情况

// for (let i = 0; i < 3; i++) alert(i)

// run begin
let i = 0
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// if condition → run body and run step
if (i < 3) { alert(i); i++ }
// ...finish, because now i == 3
内联变量声明

在此处,在循环中直接声明“计数器”变量 i。这称为“内联”变量声明。此类变量仅在循环内可见。

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // error, no such variable

我们可以使用现有变量,而不是定义变量

let i = 0;

for (i = 0; i < 3; i++) { // use an existing variable
  alert(i); // 0, 1, 2
}

alert(i); // 3, visible, because declared outside of the loop

跳过部分

for 的任何部分都可以跳过。

例如,如果我们不需要在循环开始时执行任何操作,我们可以省略 begin

如下所示

let i = 0; // we have i already declared and assigned

for (; i < 3; i++) { // no need for "begin"
  alert( i ); // 0, 1, 2
}

我们还可以删除 step 部分

let i = 0;

for (; i < 3;) {
  alert( i++ );
}

这使得循环与 while (i < 3) 相同。

实际上我们可以删除所有内容,从而创建一个无限循环

for (;;) {
  // repeats without limits
}

请注意,必须存在两个 for 分号 ;。否则,会出现语法错误。

中断循环

通常,当循环条件变为假时,循环会退出。

但我们可以随时使用特殊 break 指令强制退出。

例如,下面的循环要求用户输入一系列数字,在未输入数字时“中断”

let sum = 0;

while (true) {

  let value = +prompt("Enter a number", '');

  if (!value) break; // (*)

  sum += value;

}
alert( 'Sum: ' + sum );

如果用户输入空行或取消输入,则在行 (*) 处激活 break 指令。它立即停止循环,将控制权传递给循环后的第一行。即 alert

“无限循环 + 根据需要 break”组合非常适合在循环条件必须在循环的中间甚至其正文的几个位置(而不是在循环的开头或结尾)进行检查的情况下。

继续进行下一次迭代

continue 指令是 break 的“精简版”。它不会停止整个循环。相反,它停止当前迭代并强制循环开始新的迭代(如果条件允许)。

如果我们完成了当前迭代并希望继续进行下一次迭代,我们可以使用它。

下面的循环使用 continue 仅输出奇数值

for (let i = 0; i < 10; i++) {

  // if true, skip the remaining part of the body
  if (i % 2 == 0) continue;

  alert(i); // 1, then 3, 5, 7, 9
}

对于 i 的偶数值,continue 指令会停止执行主体,并将控制权传递给 for 的下一次迭代(使用下一个数字)。因此,alert 仅对奇数值调用。

continue 指令有助于减少嵌套

显示奇值的循环可能如下所示

for (let i = 0; i < 10; i++) {

  if (i % 2) {
    alert( i );
  }

}

从技术角度来看,这与上面的示例相同。当然,我们可以使用 if 块包装代码,而不是使用 continue

但作为一个副作用,这创建了另一个级别的嵌套(大括号内的 alert 调用)。如果 if 中的代码长度超过几行,则可能会降低整体可读性。

没有 break/continue 到“?”的右侧

请注意,不能将不是表达式的语法结构与三元运算符 ? 一起使用。特别是,诸如 break/continue 的指令不允许在那里使用。

例如,如果我们采用此代码

if (i > 5) {
  alert(i);
} else {
  continue;
}

…并使用问号重写它

(i > 5) ? alert(i) : continue; // continue isn't allowed here

…它停止工作:出现语法错误。

这是另一个不使用问号运算符 ? 而不是 if 的原因。

break/continue 的标签

有时我们需要一次从多个嵌套循环中跳出。

例如,在下面的代码中,我们循环遍历 ij,提示从 (0,0)(2,2) 的坐标 (i, j)

for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // what if we want to exit from here to Done (below)?
  }
}

alert('Done!');

我们需要一种方法来停止进程,如果用户取消输入。

input 之后的普通 break 只能中断内部循环。这还不够——标签,来救援!

标签是循环前带冒号的标识符

labelName: for (...) {
  ...
}

下面循环中的 break <labelName> 语句会中断到标签

outer: for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // if an empty string or canceled, then break out of both loops
    if (!input) break outer; // (*)

    // do something with the value...
  }
}

alert('Done!');

在上面的代码中,break outer 向上查找名为 outer 的标签,并中断该循环。

因此,控制权直接从 (*) 转到 alert('Done!')

我们也可以将标签移动到单独的行上

outer:
for (let i = 0; i < 3; i++) { ... }

continue 指令也可以与标签一起使用。在这种情况下,代码执行将跳转到标记循环的下一个迭代。

标签不允许“跳转”到任何地方

标签不允许我们跳转到代码中的任意位置。

例如,不可能执行此操作

break label; // jump to the label below (doesn't work)

label: for (...)

break 指令必须位于代码块内。从技术上讲,任何带标签的代码块都可以,例如

label: {
  // ...
  break label; // works
  // ...
}

…尽管 99.9% 的情况下,break 用于循环内,如我们在上面的示例中所见。

continue 只能从循环内执行。

总结

我们介绍了 3 种类型的循环

  • while – 在每次迭代之前检查条件。
  • do..while – 在每次迭代之后检查条件。
  • for (;;) – 在每次迭代之前检查条件,提供其他设置。

为了形成“无限”循环,通常使用 while(true) 构造。这样的循环与任何其他循环一样,都可以使用 break 指令停止。

如果我们不想在当前迭代中执行任何操作,并且希望转发到下一个迭代,我们可以使用 continue 指令。

break/continue 支持循环前的标签。标签是 break/continue 逃逸嵌套循环以转到外部循环的唯一方式。

任务

重要性:3

此代码发出的最后一个值是什么?为什么?

let i = 3;

while (i) {
  alert( i-- );
}

答案:1

let i = 3;

while (i) {
  alert( i-- );
}

每次循环迭代都会将 i 减少 1。检查 while(i)i = 0 时停止循环。

因此,循环的步骤形成以下序列(“循环展开”)

let i = 3;

alert(i--); // shows 3, decreases i to 2

alert(i--) // shows 2, decreases i to 1

alert(i--) // shows 1, decreases i to 0

// done, while(i) check stops the loop
重要性:4

对于每次循环迭代,写下它输出的值,然后将其与解决方案进行比较。

两个循环都alert相同的值,还是不相同?

  1. 前缀形式 ++i

    let i = 0;
    while (++i < 5) alert( i );
  2. 后缀形式 i++

    let i = 0;
    while (i++ < 5) alert( i );

该任务演示了后缀/前缀形式在用于比较时如何导致不同的结果。

  1. 从 1 到 4

    let i = 0;
    while (++i < 5) alert( i );

    第一个值是 i = 1,因为 ++i 先将 i 自增,然后返回新值。所以第一个比较是 1 < 5,并且 alert 显示 1

    然后依次是 2, 3, 4… - 这些值一个接一个地显示出来。比较总是使用自增后的值,因为 ++ 在变量之前。

    最后,i = 4 自增为 5,比较 while(5 < 5) 失败,循环停止。所以 5 没有显示出来。

  2. 从 1 到 5

    let i = 0;
    while (i++ < 5) alert( i );

    第一个值仍然是 i = 1i++ 的后缀形式将 i 自增,然后返回值,所以比较 i++ < 5 将使用 i = 0(与 ++i < 5 相反)。

    但是 alert 调用是独立的。这是另一个在自增和比较之后执行的语句。所以它获取当前 i = 1

    然后依次是 2, 3, 4…

    让我们在 i = 4 处停止。前缀形式 ++i 将对其进行自增,并在比较中使用 5。但是这里我们有后缀形式 i++。所以它将 i 自增为 5,但返回旧值。因此,比较实际上是 while(4 < 5) - 为真,并且控制权转到 alert

    i = 5 是最后一个,因为在下一步中 while(5 < 5) 为假。

重要性:4

对于每个循环,写下它将显示哪些值。然后与答案进行比较。

两个循环 alert 的值是否相同?

  1. 后缀形式

    for (let i = 0; i < 5; i++) alert( i );
  2. 前缀形式

    for (let i = 0; i < 5; ++i) alert( i );

答案:在两种情况下都是从 04

for (let i = 0; i < 5; ++i) alert( i );

for (let i = 0; i < 5; i++) alert( i );

这可以很容易地从 for 的算法中推导出

  1. 在所有操作之前执行一次 i = 0(开始)。
  2. 检查条件 i < 5
  3. 如果为 true - 执行循环体 alert(i),然后 i++

自增 i++ 与条件检查 (2) 是分开的。那只是另一个语句。

这里没有使用自增返回的值,所以 i++++i 没有区别。

重要性:5

使用 for 循环输出从 210 的偶数。

运行演示

for (let i = 2; i <= 10; i++) {
  if (i % 2 == 0) {
    alert( i );
  }
}

我们使用“模数”运算符 % 来获取余数,并在此处检查偶数。

重要性:5

重写代码,将 for 循环更改为 while,而不改变其行为(输出应保持不变)。

for (let i = 0; i < 3; i++) {
  alert( `number ${i}!` );
}
let i = 0;
while (i < 3) {
  alert( `number ${i}!` );
  i++;
}
重要性:5

编写一个循环,提示输入大于 100 的数字。如果访问者输入其他数字,请要求他们重新输入。

循环必须要求输入一个数字,直到访问者输入一个大于 100 的数字或取消输入/输入一个空行。

在这里,我们可以假设访问者只输入数字。在这个任务中,无需为非数字输入实现特殊处理。

运行演示

let num;

do {
  num = prompt("Enter a number greater than 100?", 0);
} while (num <= 100 && num);

循环 do..while 在两个检查都为真时重复

  1. 检查 num <= 100 - 也就是说,输入的值仍然不大于 100
  2. numnull 或空字符串时,检查 && num 为假。然后 while 循环也会停止。

P.S. 如果 numnull,则 num <= 100true,因此,如果没有第二个检查,如果用户单击取消,循环将不会停止。需要两个检查。

重要性:3

如果一个大于 1 的整数不能被除以 1 和它本身之外的任何数而没有余数,则称其为 素数

换句话说,如果 n > 1 不能被除以 1n 之外的任何数,则它是一个素数。

例如,5 是一个素数,因为它不能被 234 整除。

编写一个代码,输出从 2n 区间内的素数。

对于 n = 10,结果将是 2,3,5,7

P.S. 该代码应适用于任何 n,而不针对任何固定值进行微调。

此任务有很多算法。

我们使用嵌套循环

For each i in the interval {
  check if i has a divisor from 1..i
  if yes => the value is not a prime
  if no => the value is a prime, show it
}

使用标签的代码

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // for each i...

  for (let j = 2; j < i; j++) { // look for a divisor..
    if (i % j == 0) continue nextPrime; // not a prime, go next i
  }

  alert( i ); // a prime
}

有很多空间可以优化它。例如,我们可以从 2i 的平方根寻找除数。但无论如何,如果我们希望对大区间真正高效,我们需要改变方法,并依赖于高级数学和复杂算法,如 二次筛法通用数域筛法 等。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于超过 10 行 - 使用沙盒 (plnkrjsbincodepen…)