TypeScript循环基础:while、do-while、for、break与continue
了解 TypeScript 中的 while、do-while 和 for 循环是如何判断条件的,为何循环变量的作用域会改变结果,以及 break 和 continue 是如何改变程序执行流程的。
来源: 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
}
这些步骤的执行顺序值得牢记:
- 初始化会在其他任何操作之前执行一次(例如
let i = 1)。 - 会判断条件。如果条件为
true,则执行循环体;如果为false,则循环结束。 - 执行完循环体后,会执行更新表达式(例如
i++)。 - 控制权返回到第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 则跳过当前迭代;它们相对于其他语句的位置不同会影响程序输出。条件语句决定执行哪段代码,循环则控制执行的频率。二者结合便涵盖了你编写代码时所需的大部分控制流。