首页 / 文章

TypeScript循环基础:while、do-while、for、break与continue

了解 TypeScript 中的 while、do-while 和 for 循环是如何判断条件的,为何循环变量的作用域会改变结果,以及 break 和 continue 是如何改变程序执行流程的。

发布者:REACTAPP.TOP阅读约 9 分钟1983 词

来源: Mastering Loops in TypeScript: while, do-while, for, break & continue

循环允许在条件成立时反复执行相同的代码块,而无需重复复制语句。虽然它们看起来很简单,但大多数循环错误都源于三个细节:条件检查的时间、循环变量的存储位置,以及 break 或 continue 语句在循环体中的位置。本指南将通过一些简单的示例讲解 TypeScript 的 while、do-while 和 for 循环,这些示例的输出可以逐行预测,这样你在编写迭代代码时就不会遇到意外情况。

为何循环优于重复

如果不使用循环来打印1到10,就需要调用十次 console.log。而使用循环后,只需描述一次操作流程,运行时便会重复执行,无论需要重复十次还是一万次,代码的长度都保持不变。

while循环:先检查,再执行

while循环的结构是所有循环中最简单的:

while (condition) {
  // statements
}

每次循环开始前都会先判断条件。只要条件为true,循环体就会执行;一旦变为false,循环即终止。一个正常的while循环需要三个要素:

  • 条件所依赖的变量的初始值;
  • 条件本身;
  • 循环体中能让该变量逐渐使条件变为false的代码。

如果缺少第三个要素,循环将永远不会停止,这个问题将在下文介绍。

从1计数到10

这里可以看到全部三个要素:i从1开始,只要i <= 10循环就会继续执行,而每次循环都会通过i++让它的值加1:

let i: number = 1; // initialization
while (i <= 10) {
  console.log(i);
  i++;
}

筛选偶数

若要仅打印该范围内的偶数,可继续遍历每个数值,但在循环体内添加判断条件。取模运算符 % 可用于获取除法运算的余数,因此当 i 为偶数时,i % 2 == 0 才会成立:

let i: number = 1;
while (i <= 10) {
  if (i % 2 == 0) {
    console.log(i);
  }
  i++;
}

还有更简洁的方法能达到类似效果:完全跳过判断条件,每次直接递增2。需要注意的是,这种方式的起始值为0,因此输出结果也会包含0:

let i: number = 0;
while (i <= 10) {
  console.log(i);
  i = i + 2;
}

输出结果: 0, 2, 4, 6, 8, 10

筛选奇数

将比较条件改为 != 0 即可筛选出奇数。由于0是偶数,会被过滤掉,因此从0开始在这里并无问题:

let i: number = 0;
while (i <= 10) {
  if (i % 2 != 0) {
    console.log(i);
  }
  i++;
}

输出结果: 1, 3, 5, 7, 9

这些示例使用了 == 和 !;严格形式的 === 和 !== 通常更受青睐,因为它们不会强制类型转换,尽管对于两个数字来说两者的结果是一样的。

倒计时

要实现倒计时,从较大的数值开始,在值至少为1时不断递减:

let i: number = 10;
while (i >= 1) {
  console.log(i);
  i--;
}

无限循环及其产生原因

while 条件只是一个能计算出布尔值的表达式,因此 while (true) 是完全有效的。下面的循环会永远运行下去,因为没有任何东西可以终止它:

let i: number = 1;
while (true) {
  console.log(i);
  i++;
  // no break, no exit condition this runs forever
}

通常情况下,无限循环是意外产生的:控制变量从未被更新,或者更新方向错误。失控的循环会阻塞其所在线程中的所有其他操作,从而导致浏览器标签页或Node.js进程看似挂起。不过,当配合明确的退出条件——通常是break语句(详见下文)使用时,故意使用的while (true)则是可行的,例如在“不断重试直至成功”的逻辑中。

do-while循环:先执行一次,再检查条件

do-while循环将条件判断放在了最后:

do {
  // statements
} while (condition);

由于条件检查是在循环体**之后**进行的,因此无论条件如何,循环体至少会执行一次。相比之下,普通的while循环则可能一次都不执行。

当初始条件为真时,do-while循环的行为与while循环完全相同:

let i: number = 1;
do {
  console.log(i);
  i++;
} while (i <= 5);

当条件从一开始就是假值时,差异就会显现:

let i: number = 10;
do {
  console.log(i);
  i++;
} while (i <= 5);

输出结果: 10

代码块会无条件执行,先输出10并将i的值加1变为11。之后才会判断i <= 5,由于该值为假,循环在第一次迭代后就会结束。如果使用while (i <= 5)且i的初始值为10,那么将不会输出任何内容。无论何种情况都需要至少执行一次时,应使用do-while结构,比如至少显示一次提示信息。

for循环:所有内容集中在一个头部

for循环是你最常使用的循环结构,尤其是在事先知道需要重复的次数或计数器应变化的幅度时。它将初始化、条件判断和更新操作集中到同一个头部中,因此比对应的while循环更为简洁:

for (initialization; condition; increment/decrement) {
  // statements
}

这些步骤的执行顺序值得牢记:

  1. 初始化会在其他任何操作之前执行一次(例如 let i = 1)。
  2. 会判断条件。如果条件为 true,则执行循环体;如果为 false,则循环结束。
  3. 执行完循环体后,会执行更新表达式(例如 i++)。
  4. 控制权返回到第2步,循环会不断重复,直到条件变为 false。

换句话说,初始化只执行一次,而条件判断和更新则在每次循环中都会执行。

从1数到10

for (let i = 1; i <= 10; i++) {
  console.log(i);
}

获取偶数的两种方法

与 while 循环类似,你可以每次递增2:

for (let i = 2; i <= 10; i += 2) {
  console.log(i);
}

或者遍历每个值并在循环体内部进行过滤:

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

这两种方法都会输出相同的序列:

2
4
6
8
10

逐步遍历因为只访问偶数值,所以处理量更少;而当规则不是固定的步长时,比如“能被3或7整除”,过滤方式则更加灵活。

倒序计数

通过递减更新可以让循环反向执行:

for (let i = 10; i >= 1; i--) {
  console.log(i);
}

输出结果: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1

为什么循环结束后循环变量还是6

循环变量的声明位置决定了循环结束后的值。在这个例子中,i是在循环之前声明的,而for语句块只是对它进行赋值操作:

let i: number;
for (i = 1; i <= 5; i++) {
  console.log(i);
}
console.log(i);

输出结果: 1, 2, 3, 4, 5, 6

由于 i 位于外部作用域,最终的 console.log(i) 仍然可以访问到它,其值仍是循环结束时保留的数值。根据上述运算顺序,该值为 6 而非 5:在最后一次循环中 i 的值为 5,代码体将其输出,随后更新操作将其提升为 6,只有这时 6 <= 5 的条件才不成立,循环随之结束。

更常见的写法 for (let i = 1; i <= 5; i++) 中,i 具有仅限于循环内部的块级作用域,因此循环结束后它就不再存在。在 TypeScript 中,循环后尝试访问 i 会引发编译时错误(“无法找到名称 ‘i’”),而在普通 JavaScript 中则会在运行时抛出 ReferenceError。通常建议在作用域顶部声明变量;只有在需要最终值时才使用外部声明。

利用 break 和 continue 改变循环流程

两个跳转语句允许你在循环体内部改变其正常执行流程:

  • break 会立即终止循环,无论还有多少次迭代未执行。
  • continue 仅跳过当前迭代并进入下一次迭代;循环会继续运行。

在 for 循环中,continue 仍会在再次检查条件之前执行更新表达式,这就是计数器会持续递增的原因。

break:在循环体中的位置很重要

在此例中,break 的检查发生在打印操作之前:

let i: number;
for (i = 1; i <= 10; i++) {
  if (i == 5) {
    break;
  }
  console.log(i);
}

输出结果: 1, 2, 3, 4

当 i 的值达到 5 时,循环会在 console.log 执行之前就终止,因此 5 不会出现在输出中。

如果将打印操作放在检查之前,结果就会发生变化:

let i: number;
for (i = 1; i <= 10; i++) {
  console.log(i);
  if (i == 5) {
    break;
  }
}

输出结果: 1, 2, 3, 4, 5

条件并未改变,但由于打印操作在检查之前执行,因此现在会输出5。对于break和continue来说,代码块内的执行顺序决定了输出结果。

continue:跳过单个值

将break替换为continue后,“在5处停止”就变成了“跳过5”:

let i: number;
for (i = 1; i <= 10; i++) {
  if (i == 5) {
    continue;
  }
  console.log(i);
}

输出结果: 1, 2, 3, 4, 6, 7, 8, 9, 10

循环会继续执行,只是跳过了那一次的打印操作。

跳过所有奇数

结合过滤器使用时,continue会直接让所有奇数值进入下一次循环:

let i: number;
for (i = 0; i <= 10; i++) {
  if (i % 2 != 0) {
    continue;
  }
  console.log(i);
}

输出结果: 0, 2, 4, 6, 8, 10

跳过特定值列表

该条件可以根据需求设计得十分复杂。通过使用逻辑或运算符 ||,此循环会跳过 3、5 和 7:

let i: number;
for (i = 0; i <= 10; i++) {
  if (i == 3 || i == 5 || i == 7) {
    continue;
  }
  console.log(i);
}

输出结果: 0, 1, 2, 4, 6, 8, 9, 10

对于更长的跳过列表,使用 [3, 5, 7].includes(i) 或 Set 会更清晰。

关键要点

  • while 循环在每次迭代前都会检查条件,可能一次都不执行;而 do-while 循环则在迭代后检查条件,至少会执行一次。
  • 每个循环都需要某种更新操作,使条件最终变为假值;否则就会产生无限循环,阻塞线程。
  • 在 for 循环中,初始化操作只执行一次,而条件检查和更新操作则在每次循环中都会执行,这就是为什么计数器最终会超出设定范围的原因。
  • 在循环开头使用 let 声明计数器可使其作用域限制在循环内部;若在循环外部声明,则其最终值会保留下来。
  • break 用于结束循环,而 continue 则跳过当前迭代;它们相对于其他语句的位置不同会影响程序输出。
  • 条件语句决定执行哪段代码,循环则控制执行的频率。二者结合便涵盖了你编写代码时所需的大部分控制流。