1.为什么需要循环
第1讲我们把程序拆成了顺序、条件、重复三部分。如果说第3讲的条件语句是“岔路口”,那么本讲的循环就是“回头路”。要在屏幕上打印 1 到 5,写五行 console.log 也可以;但如果是 1 到 1,000,情况就不同了。复制粘贴很容易出错,而且想改数字时每一行都得改。
循环是只写一次“只要这个条件成立,就一直做这件事”的语法。人长时间做同一件事会累、会出错,计算机做第一百次和第一次一模一样。不过,计算机自己判断不了什么时候该停,停下的条件必须由人准确写出来。本讲自始至终都会练习把“重复做什么”和“何时停下”当作一对来思考。
下面的代码不用写五行,一个循环就得到同样的结果。把数字 5 改成 1000 就会打印 1,000 行,而代码只改了几个字符。这就是循环成为自动化起点的原因。
for (let i = 1; i <= 5; i++) {
console.log('第' + i + '次问候:你好');
}第1次问候:你好 第2次问候:你好 第3次问候:你好 第4次问候:你好 第5次问候:你好
2.`for` 语句——次数确定的重复
for 语句的括号里有用分号隔开的三部分。第一部分是开始时只执行一次的准备(let i = 1),第二部分是每次重复前检查的条件(i <= 5),第三部分是每转完一圈要做的事(i++)。i++ 是 i = i + 1 的简写。条件为 true 时执行花括号里的循环体,条件一变成 false,循环就结束。
准确了解执行顺序很重要。它按照 准备 → 检查条件 → 循环体 → 递增 → 检查条件 → 循环体 → 递增 …… 的顺序转,条件为假时跳过循环体,往循环下方继续执行。所以最后 i 的值是第一次不满足条件的那个值。像下表这样一行行写下变量的值,叫作“追踪”,循环行为异常时最先应该做的就是这个。
循环变量叫 i 只是老习惯,并不是规定。只要意思清楚,也可以起 month、page 这样的名字。循环变量的值会变化,所以用 let 而不是 const 声明(第2讲)。
const dan = 2;
for (let i = 1; i <= 4; i++) {
console.log(dan + ' x ' + i + ' = ' + dan * i);
}2 x 1 = 2 2 x 2 = 4 2 x 3 = 6 2 x 4 = 8
| 圈数 | `i` 的值 | 条件 `i <= 4` | 做了什么 |
|---|---|---|---|
| 1 | 1 | 真 | 输出 2 x 1 = 2,i 变成 2 |
| 2 | 2 | 真 | 输出 2 x 2 = 4,i 变成 3 |
| 3 | 3 | 真 | 输出 2 x 3 = 6,i 变成 4 |
| 4 | 4 | 真 | 输出 2 x 4 = 8,i 变成 5 |
| - | 5 | 假 | 跳过循环体,循环结束 |
3.`for...of` 与两种基本模式——累加和计数
要把列表里的值一个个取出来用,for...of 最好读。写成 for (const x of list),list 里的值就会从头依次放进 x。不需要数是第几个,弄错序号的失误也就少了。列表(数组)在第6讲详细讲,这里只要知道它是在方括号里用逗号排列的一串值就行。
循环做的事情,很大一部分可以归结为两种模式。累加模式是在循环前先准备一个“还什么都没有”的值,比如 let sum = 0,每转一圈就把值加上去。计数模式从 let count = 0 开始,只在满足条件时加 1。两种模式的起始值都很重要:求和从 0 开始,求积从 1 开始,拼接文字时从空字符串 '' 开始。
如果在循环里面声明累加变量,它每一圈都会重新变成 0,合计就累积不起来。一定要在循环外面、上方声明。反过来,输出结果的 console.log 通常放在循环结束后的下方。放在里面的话,每一圈都会打印中间合计。想看中间值时,也可以特意放进去当追踪用。
const sales = [12000, 8500, 30000, 4000];
let sum = 0;
let count = 0;
for (const won of sales) {
sum += won;
if (won >= 10000) {
count++;
}
}
console.log('合计', sum);
console.log('10,000韩元以上', count + '笔');合计 54500 10,000韩元以上 2笔
[70, 85, 90, 60] 的平均分。应该按什么顺序思考?const scores = [70, 85, 90, 60];
let total = 0;
for (const s of scores) {
total += s;
}
console.log(total / scores.length);76.25
- 第1步 累积什么:累积分数的合计。起始值是
0。 - 第2步 一圈做的事:取出一个分数,加到合计上。70 → 155 → 245 → 305。
- 第3步 循环结束后:合计 305 除以个数 4。个数用
scores.length得到。 - 验算:305 ÷ 4 = 76.25,在 70 和 90 之间,说得通。如果是空列表,就成了 0 ÷ 0,得到
NaN,所以要单独处理。
4.`while` 语句与无限循环——谁来改变结束条件
while 语句只接收一个条件。条件为真时,就一直执行循环体。它适合事先不知道要转几圈、要“一直重复到达成目标为止”的情况。例如每月存同样的金额,要多少个月才能达到目标金额?虽然也能用公式算,但一个月一个月加着数是最直观的。
使用 while 时一定要问一句:“循环体里有没有改变条件的那一行?”下面的代码如果没有 saved += 50000,saved 就永远是 0,条件永远为真。这就是无限循环。陷入无限循环的程序不报错也不停,一直转下去,看起来就像浏览器标签页没有响应。本页的练习区如果代码在 2 秒内没有结束,会强制停止并提示“超时”。
预防无限循环有三个检查点:条件里用到的变量在循环体里有没有变化;变化的方向是不是让条件趋向假(从 0 开始却一直减,只会离目标越来越远);以及有没有用“超过也会停”的 < 之类的条件,而不是“恰好相等才停”的 !==。就像第2讲看到的 0.1 + 0.2,小数可能永远无法恰好相等,所以用 !== 来停止很危险。
const goal = 300000;
let saved = 0;
let months = 0;
while (saved < goal) {
saved += 50000;
months++;
}
console.log(months + '个月后', saved + '韩元');6个月后 300000韩元
console.log 打印循环变量和累加变量。看到转了几圈后变成什么值,原因基本就暴露了。圈数太多时,可以暂时把条件改小,只看前几圈。5.`break` 与 `continue`——提前停下、跳过
break 会立即跳出循环。只需要在列表里找到第一个符合条件的值时,找到后还转到最后就是浪费,所以用 break 停下。continue 会跳过本圈剩下的部分,直接进入下一圈。用于过滤掉空值或错误的值,只处理其余部分。
这两个关键字很方便,但用多了流程就难以跟踪。如果一个循环里混着好几个 break 和 continue,改写成条件语句,或拆成第5讲要学的函数,会更好读。尤其在函数里,return 会把循环和函数一起结束,能更清楚地表达“找到就立刻返回”的意思。
“没找到的情况”也一定要考虑。下面第一段代码把 found 的起始值设为 -1,表示“还没找到”。循环结束后如果仍是 -1,就说明列表里没有符合条件的值。数组的 indexOf 找不到时返回 -1,也是同样的约定(第6讲)。
const temps = [3, 1, -2, 5, -4];
let found = -1;
for (let i = 0; i < temps.length; i++) {
if (temps[i] < 0) {
found = i;
break;
}
}
console.log('第一个零下气温的位置', found);第一个零下气温的位置 2
const items = ['苹果', '', '梨', '', '橘子'];
for (const name of items) {
if (name === '') {
continue;
}
console.log(name);
}苹果 梨 橘子
6.off-by-one——差一个就出错的最常见失误
循环错误中最常见的是多转一圈或少转一圈,英文叫 off-by-one(差一)错误。原因通常有三个:从 0 开始还是从 1 开始,条件用 < 还是 <=,以及把“个数”和“最后的序号”弄混。数组的位置从 0 开始数,所以长度为 5 的数组,最后的位置是 4 而不是 5。
用栅栏柱问题来找找感觉。10 米长的栅栏每隔 1 米立一根柱子,柱子不是 10 根而是 11 根。区间数和端点数差一个。同样,for (let i = 0; i < 5; i++) 是 0、1、2、3、4 共 5 次,for (let i = 1; i <= 5; i++) 是 1~5 共 5 次,for (let i = 0; i <= 5; i++) 是 0~5 共 6 次。里面都有“5”,次数却不同。
抓 off-by-one 最可靠的办法是用边界值亲自试。就像第3讲检查条件语句的边界值那样,循环一定要试试“0 个时、1 个时、最后一个”。按位置遍历数组时如果用 i <= arr.length 多走一格,最后一圈的 arr[i] 就是 undefined,合计变成 NaN。没有错误信息,只是出现奇怪的值,所以更难发现。如果不需要位置,用 for...of 就能从根本上避开这个失误。
<= 与 < 一字之差——多出的格子是 undefined,合计变成 NaNconst nums = [10, 20, 30];
let a = 0;
for (let i = 0; i <= nums.length; i++) {
a += nums[i];
}
console.log('多走一格的合计', a);
let b = 0;
for (let i = 0; i < nums.length; i++) {
b += nums[i];
}
console.log('正确的合计', b);多走一格的合计 NaN 正确的合计 60
- 列表为空(0 个)时结果是否合理——合计 0、个数 0,平均值单独处理
- 只有一个值时,循环是否恰好转一次
- 最后一个值是否被处理——确认条件是
<还是<= - 条件里用到的变量是否在循环体中变化、迟早变为假
- 累加变量是否在循环外面声明
7.读懂并判断循环——面对 AI 写的代码
对 AI 编程助手说“帮我求列表的合计”,很快就能拿到带循环的代码。但这段代码在你的数据上能否正确运行,要自己确认。读循环时只需找三样东西:循环前准备了什么(起始值),每一圈做什么(循环体),什么时候停(结束条件)。能把这三点各用一句话说出来,就说明你看懂了这段代码。
接下来用小输入手动追踪。放进一个三个值左右的列表,像上面的追踪表那样逐圈写下变量的值,再和实际运行结果比较。空列表和只有一个值的列表也要试。如果这里结果不一致,具体地告诉 AI“这个输入得到了这个结果,期望值是这个”,修起来会快得多。读懂错误、缩小原因的方法在第8讲,验证 AI 代码的方法在第10讲会更详细地讲。
📌 要点总结
- 循环是把“重复做什么”和“何时停下”成对写下的语法
- 次数确定用
for,逐个使用列表里的值用for...of,一直到达成目标为止用while - 累加是在循环前设好起始值(合计为 0)再逐次加上;计数是只在满足条件时加 1
while的循环体里如果没有改变条件的那一行,就会成为无限循环<还是<=,从 0 还是从 1 开始——用空列表、一个值、最后一个值来检验边界
🧪 代码练习
用循环编写求合计、个数、位置的函数。空列表、0 这样的边界值测试也会一起评分,运行前先想想这些情况。
代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。
浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。
1从 1 加到 n
编写函数 sumTo(n):接收数字 n,用循环返回从 1 到 n 的和。n 为 0 时返回 0。例:sumTo(5) 是 15。
sumTo(5)期望值15sumTo(1)期望值1sumTo(0)期望值0sumTo(100)期望值5050
💡 提示
条件写成 i < n 会漏掉最后的 n。要包含 n,该写什么?
查看参考答案
function sumTo(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}2统计达到标准的分数
编写函数 countAtLeast(scores, cut):接收分数数组 scores 和标准分 cut,返回大于等于标准分的分数有几个。空数组返回 0。
countAtLeast([50,70,90], 70)期望值2countAtLeast([], 60)期望值0countAtLeast([60,60,59], 60)期望值2countAtLeast([10,20], 100)期望值0countAtLeast([0,0], 0)期望值2
💡 提示
“大于等于”也包括相等的值。确认一下 > 和 >= 的区别。
查看参考答案
function countAtLeast(scores, cut) {
let count = 0;
for (const s of scores) {
if (s >= cut) {
count++;
}
}
return count;
}3找出第一个负数的位置
编写函数 firstNegative(nums):返回数字数组 nums 中第一个负数的位置(从 0 开始数)。没有负数或数组为空时返回 -1。找到后就停下,不要继续转。
firstNegative([3,1,-2,5,-4])期望值2firstNegative([-1,2])期望值0firstNegative([1,2,3])期望值-1firstNegative([])期望值-1firstNegative([0,4,-7])期望值2
💡 提示
在函数里,return 会把循环和函数一起结束。表示“没找到”的 return -1 放在循环结束之后。
查看参考答案
function firstNegative(nums) {
for (let i = 0; i < nums.length; i++) {
if (nums[i] < 0) {
return i;
}
}
return -1;
}4各位数字之和(条件循环)
编写函数 digitSum(n):接收大于等于 0 的整数 n,用 while 返回各位数字之和。例:digitSum(1234) 是 1+2+3+4 = 10。n % 10 是最后一位,Math.floor(n / 10) 是去掉最后一位后的数。
digitSum(1234)期望值10digitSum(0)期望值0digitSum(9)期望值9digitSum(1000)期望值1digitSum(99999)期望值45
💡 提示
漏掉让 n 变小的那一行就会无限循环。检查条件里用到的变量是否在循环体中变化。
查看参考答案
function digitSum(n) {
let sum = 0;
while (n > 0) {
sum += n % 10;
n = Math.floor(n / 10);
}
return sum;
}🤖 可以这样问 AI
复制后把 [ ] 部分换成你的情况。不要直接相信回答,请与正文对照核实。
看不懂循环是怎么转的时候
假设用输入 [小的示例输入] 运行下面这段 JavaScript 循环,请用追踪表(圈数、变量、条件真假、做了什么)展示每一圈变量的值如何变化。也请指出循环结束那一刻的条件。代码:[粘贴]
想验证 AI 给的循环代码时
请分别用一句话说明下面这段循环代码的起始值、循环体和结束条件。然后从空列表、只有一个值、最后一个值、可能无限循环这几个角度,把边界测试输入和期望结果做成表格。我会自己运行并比较。代码:[粘贴]
需要更多循环练习题时
请出 5 道 JavaScript 循环(`for`、`while`、`for...of`、`break`、`continue`)练习题,从易到难。一定要各包含一道累加、计数模式和 `off-by-one` 陷阱的题,数据用虚构的例子。先不要给答案,我把代码发给你后,请用边界值输入来批改。
🧰 相关工具
可以当场检查、整理本讲涉及的 JSON、正则表达式和文本的工具。请不要粘贴真实的个人信息或密码。
- MDN 文档 — JavaScript 指南:循环与迭代
- MDN 文档 — for、while、for...of、break、continue 语句参考
- ECMA-262 语言规范中关于循环语句定义的一般内容
达成全部学习目标后,请点击完成。
此浏览器无法保存记录,仅在当前页面显示。