循环:while 和 for

我们经常需要连续多次执行类似的操作。

例如,我们需要从列表中逐个输出商品时,或者对从 1 到 10 的每个数字运行相同的代码时。

循环是一种多次重复运行同一部分代码的方法。

“while” 循环

while 循环有如下语法:

while (condition) {
  // 代码
  // 所谓的“循环体”
}

conditiontrue时,执行来自循环体的 code

例如,以下将循环输出当 i < 3 时的 i

let i = 0;
while (i < 3) { // 结果分别是 0、1、2
  alert( i );
  i++;
}

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

如果上述示例中没有 i++,那么循环(理论上)会永远重复。实际上,浏览器提供了阻止这种循环的方法,对于服务器端 JavaScript,我们可以终止该进程。

任何表达式或变量都可以是循环条件,而不仅仅是比较。对它们进行计算,并通过 while 将其结果转化为布尔值。

例如,while (i != 0) 可简写为 while (i)

let i = 3;
while (i) { // 当 i 变成 0 时,条件为 false,循环终止
  alert( i );
  i--;
}
对于单行循环体,大括号并不需要

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

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

“do…while” 循环

使用 do..while 语法可以将条件检查移至循环体下面

do {
  // 循环体
} while (condition);

循环首先执行循环体,然后检查条件,当条件为真时,重复执行循环体。

例如:

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

这种形式的语法很少使用,除非您希望不管条件是否为真,循环体至少执行一次。通常其他形式是首选:while(…) {…}

“for” 循环

for 循环更加复杂,但它是最常使用的循环形式。

看起来就像这样:

for (begin; condition; step) {
  // ……循环体……
}

我们通过示例来了解这三个语句段(part)的含义。下述循环从 i 等于 03(但不包括 3)运行 alert(i)

for (let i = 0; i < 3; i++) { // 结果为 0、1、2
  alert(i);
}

我们逐个语句段分析 for 语句:

语句段
begin i = 0 进入循环时执行一次。
condition i < 3 在每次循环迭代之前检查,如果失败,循环停止。
step i++ 主体每次迭代后执行,但在条件检查之前执行。
body(循环体) alert(i) 条件为真时,重复运行。

一般循环算法的工作原理如下:

开始运行
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ ...

所以,begin 执行一次,然后进行迭代:每次检查 condition 后,bodystep 被执行。

如果您是循环方面的小白,那么回到这个例子,在一张纸上重现它逐步运行的过程,可能会对你有所帮助。

以下是我们示例中发生的情况:

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

// 开始
let i = 0
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// ...结束,因为现在 i == 3
内联变量声明

这里“计数”变量 i 在循环中声明。这叫做“内联”变量声明。这样的变量只在循环中可见。

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // 错误,没有这个变量。

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

let i = 0;

for (i = 0; i < 3; i++) { // 使用现有变量
  alert(i); // 0, 1, 2
}

alert(i); //3,可见,因为在循环之外声明

省略语句段

for 循环的任何语句段都可以被省略。

例如,如果我们在循环开始时不需要做任何事,我们可以省略 begin 语句段。

就像这样:

let i = 0; // 我们已经声明了 i 并对它进行赋值了

for (; i < 3; i++) { // "begin" 语句段不再需要
  alert( i ); // 0, 1, 2
}

我们也可以移除 step 语句段:

let i = 0;

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

该循环与 while (i < 3) 等价。

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

for (;;) {
  // 无限循环
}

请注意 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 (i % 2 == 0) continue;

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

对于偶数的 icontinue 指令停止了循环体的继续执行,将控制权传递给下一次 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 不允许在这个位置

……然后会停止运行。这样的代码将给出语法错误:

这只是不使用 ? 而不是 if 的另一个原因。

break/continue 标签

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

例如,下述代码中我们的循环使用 ij,提示坐标 (i, j)(0,0)(3,3)

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

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

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

    // 如果我想从这里退出并结束(如下所示)?

  }
}

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 (!input) break outer; // (*)

    // 用得到的值做些事……
  }
}
alert('Done!');

上述代码中,break outer 向上寻找名为 outer 的标签并跳出当前循环。

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

我们还可以将标签转移至另一行:

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

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

标签不允许“跳到”任何位置

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

例如,这样做是不可能的:

break label;  // 跳转到标签?不。

label: for (...)

只有在循环内部才能调用 break/continue,并且标签必须位于指令上方的某个位置。

总结

我们讨论了三种类型的循环:

  • while —— 每次迭代之前都要检查条件。
  • do..while —— 每次迭代后都要检查条件。
  • for (;;) —— 每次迭代之前都要检查条件,可以使用其他设置。

通常使用 while(true) 来构造“无限”循环。这样的循环就像任何其他循环一样,可以通过 break 指令来终止。

如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么 continue 指令就会执行它。

break/continue 支持循环前的标签。标签是 break/continue 跳出嵌套循环来转到外部循环的唯一方法。

任务

重要程度: 3

此代码最后一次 alert 值是多少?为什么?

let i = 3;

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

答案是:1

let i = 3;

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

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

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

let i = 3;

alert(i--); // 显示 3,i 减至 2

alert(i--) // 显示 2,i 减至 1

alert(i--) // 显示 1,i 减至 0

// 完成,while(i) 检查并停止循环
重要程度: 4

对于每次循环,写出你认为会显示的值,然后与答案进行比较。

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

  1. 前缀形式 ++i:

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

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

该任务演示在比较中 postfix/prefix 形式如何导致不同的结果。

  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

  1. 从 1 到 5

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

    第一个值也是 i=1。后缀形式 i++ 递增 i 然后返回值,因此比较 i++ < 5 将使用 i=0 (与 ++i < 5 不同)。

alert 调用是独立的。这是在递增和比较之后执行的另一条语句。因此它得到了当前的 i=1

接下来是 2,3,4…

我们在 i=4 时暂停,前缀形式 ++i 会递增并在比较中使用 5。但我们这里还有后缀形式 i++。因此,它将 i 递增到 5,但返回旧值。因此实际比较的是 while(4 < 5) —— true,控制继续执行 alert

i=5 是最后一个值,因为下一步 while(5 < 5) 为 false。

重要程度: 4

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

两次循环 alert 值是否相同?

  1. 后缀形式:

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

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

答案:在这两种情况下,从 0 到 4

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 的偶数。

运行 demo

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

我们使用 “modulo” 运算符 % 来获取余数,并检查奇偶性。

重要程度: 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 的数值或取消输入/输入空行为止。

我们假设使用者只输入数值。在此任务中,不需要对非数值输入实现特殊处理。

运行 demo

let num;

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

两次检查都为真时,循环 do..while,并重复此操作:

  1. 检查 num <= 100 —— 即输入值仍然不大于 100
  2. num 为 null 或空 string 时,检查 && num 是否为假。然后 while 循环也停止了。

P.S. 如果 num 为 null,那么 num <= 100 为 true。 因此用户单击取消,如果没有第二次检查,循环就不会停止。两次检查都是必须的。

重要程度: 3

大于 1 且不能被除了 1 和它本身以外的任何数整除的整数叫做 prime

换句话说,n > 1 不能被除了 1 和 n 以外的任何数整除,则称为素数。

例如,5 是素数,因为它不能被 23 和 4 整除,会产生余数。

编写可以将 2 到 n 之间的所有素数输出的代码。

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 (let j = 2; j < i; j++) { // 寻找一个除数……
    if (i % j == 0) continue nextPrime; // 不是素数,则继续检查下一个
  }

  alert( i ); // 输出素数
}

这段代码有很大空间可以优化。例如,我们可以从 2i 的平方根中寻找除数。但无论如何,如果我们想要在很大的时间间隔内实现高效率,我们需要改变方法,依赖高等数学和复杂算法,如Quadratic sieve, General number field sieve 等等。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。