freeCodeCamp JavaScript 基础:用 for 循环迭代数组的完整指南与实战解析
本篇围绕 freeCodeCamp 开源课程 Basic JavaScript 模块中的经典一课 "Iterate with JavaScript For Loops" 展开,逐段解析 JavaScript for 循环的语法结构与执行时序,并结合 freeCodeCamp 仓库中该挑战的 Markdown 源码、课程结构定义与前端测试机制,讲清"循环为什么这样写""三个表达式各负责什么""如何完成并验证练习"。读完你将掌握 for 循环的完整心智模型,并能独立完成本挑战以及其后的奇数计数、倒序遍历、遍历数组等一系列进阶练习。
一、挑战背景:这一课在 freeCodeCamp 课程中的位置
该挑战的文档源文件位于 curriculum/challenges/english/blocks/basic-javascript/cf1111c1c11feddfaeb5bdef.md,它属于 freeCodeCamp 课程体系中的 basic-javascript(JavaScript 基础)block。
该 block 的内容与顺序定义在 curriculum/structure/blocks/basic-javascript.json 中。从该结构文件可以看到清晰的"由浅入深"迭代学习路径:
Iterate with JavaScript While Loops(while 循环)——上一课,先引入"当条件为真就反复执行"的循环思想;Iterate with JavaScript For Loops(for 循环)——本课,引入最常用的定次循环;Iterate Odd Numbers With a For Loop(用 for 循环迭代奇数);Count Backwards With a For Loop(用 for 循环倒数);Iterate Through an Array with a For Loop(用 for 循环遍历数组);Nesting For Loops(for 循环嵌套);Iterate with JavaScript Do...While Loops(do...while 循环)。
因此本课处在"循环主题"的承上启下位置:它建立在 while 循环的理解之上,又是后续所有 for 循环变体(定步长、倒序、遍历数组、嵌套)的语法基础。理解透彻这一课,后续练习会非常顺滑。
二、for 循环的语法骨架:三个可选表达式
文档原文的核心定义是:for 循环之所以叫这个名字,是因为它可以"精确地运行指定次数"。它的声明形式由分号分隔的三个可选表达式组成:
for (a; b; c) {
// 循环体
}
其中:
| 位置 | 名称 | 执行时机 | 典型用途 |
|---|---|---|---|
a |
初始化语句(initialization statement) | 在循环开始前仅执行一次 | 定义并设置循环变量,如 let i = 0 |
b |
条件语句(condition statement) | 每次迭代开始前被求值 | 判断循环是否继续,如 i < 5 |
c |
最终表达式(final expression) | 每次迭代结束后、下一次条件判断之前执行 | 递增/递减循环计数器,如 i++ |
三个关键执行规则值得逐条展开:
- 初始化语句只运行一次:它在整个循环生命周期内只执行一次,是定义循环变量的"起点"。例如
let i = 0声明了从 0 开始的计数器。 - 条件语句决定循环是否继续:它会在每一轮迭代开始时被求值。只要条件为
true,循环体就继续执行;一旦在某轮迭代开始前条件求值为false,循环立即停止。由此可推出一个重要结论——如果条件从一开始就是false,循环体一次都不会执行(零次迭代,与 do...while 恰好相反)。 - 最终表达式在每个迭代的末尾执行:它运行在循环体完成之后、下一次条件检查之前,通常用来更新计数器,例如
i++(等价于i = i + 1)。
结合以上规则,一次完整的 for (a; b; c) 迭代过程可概括为如下时序:
执行 a(仅一次)
循环:
求值 b → 若为 false,退出循环
执行循环体
执行 c
回到"求值 b" 这一步
这正是 "for 循环能在开始前就确定运行次数" 的本质原因:迭代的推进完全由三个表达式按固定节奏协同驱动。
三、逐行拆解官方示例:向数组填充 0 到 4
文档给出的教学示例非常典型:
const ourArray = [];
for (let i = 0; i < 5; i++) {
ourArray.push(i);
}
运行结束后,ourArray 的值为 [0, 1, 2, 3, 4]。对照上一节的三段式结构逐项分析:
let i = 0(初始化):声明块级作用域的循环变量i并赋初值0。注意这里使用 ES6 的let而不是var,可以避免变量提升带来的作用域污染问题(详见第六节)。i < 5(条件):只要i小于 5,就继续执行循环体。因为i从 0 开始,所以合法取值为 0、1、2、3、4,恰好 5 轮。i++(最终表达式):每轮结束后把i加 1,使计数器朝退出条件(i >= 5)方向逼近。ourArray.push(i)(循环体):把当前的i追加到数组末尾。Array.prototype.push每次把新元素放入数组尾部,因此 5 轮累积得到[0, 1, 2, 3, 4]。
逐轮执行追踪如下:
| 轮次 | 迭代开始前 i 的值 |
条件 i < 5 |
本轮推入 | 循环体后执行 i++ |
|---|---|---|---|---|
| 第 1 轮 | 0 | true | 0 | i 变为 1 |
| 第 2 轮 | 1 | true | 1 | i 变为 2 |
| 第 3 轮 | 2 | true | 2 | i 变为 3 |
| 第 4 轮 | 3 | true | 3 | i 变为 4 |
| 第 5 轮 | 4 | true | 4 | i 变为 5 |
| 退出 | 5 | false | 不执行 | —— |
当第 5 轮结束后 i 变成 5,下一次条件检查 5 < 5 为 false,循环终止——这解释了为什么循环体执行了 5 次,最终数组却是 [0, 1, 2, 3, 4] 而不是 [0, 1, 2, 3, 4, 5]。数组最后一个元素永远比循环次数少 1,这是一个非常容易踩坑的边界点。
关于"三个表达式都是可选"的补充说明
文档明确指出三个表达式均为"可选"(optional)。在实际工程中,最常用的省略场景是配合 break 使用无限循环:
for (;;) {
// 循环体会一直执行,直到遇到 break / return / 抛异常
break; // 例如通过某种条件主动退出
}
for (;;) 中条件缺省会被视为恒为 true,因此必须靠循环体内部的 break 等语句主动终止,否则会造成死循环。除此之外,也可以把计数器更新挪进循环体,从而省略最终表达式。理解"可选"二字,有助于日后阅读各类第三方库的循环代码。
四、实战任务:用 for 循环把 1 到 5 推入 myArray
文档的 --instructions-- 小节给出本次编程任务:
使用一个
for循环,把数值 1 到 5 依次push到myArray上。
挑战会预先在编辑器中提供一段种子代码(seed code,见文档 --seed-- 段):
// Setup
const myArray = [];
// Only change code below this line
要求学习者只改动注释以下的部分,写出正确的 for 循环。
一种标准解法
const myArray = [];
for (let i = 1; i < 6; i++) {
myArray.push(i);
}
解法思路完全复用示例模式,只需做两处调整即可满足"1 到 5"的目标:
- 初始化从
1开始:把let i = 0改为let i = 1,让推入的第一个值是 1; - 边界条件改成
< 6:由于i从 1 起步,若要覆盖到 5,需要条件为i < 6(也可以写成i <= 5,两者等价)。若写成i < 5,则只推入 1、2、3、4,会漏掉最后一个数。
最终 myArray 依次被 push 为 [1, 2, 3, 4, 5]。注意这里仍然沿用 push 而非赋值下标,与文档示例保持一致的"循环 + push"惯用法,后续遍历数组主题中还会反复出现这种组合。
五、测试是如何判定答案正确性的:读懂 hints
在 freeCodeCamp 的挑战文件中,每个挑战都由若干可自动执行的断言(hints)来判定是否通过。本挑战的判定逻辑位于文档的 --hints-- 小节,包含两层检查:
第一层:确认确实使用了 for 循环
assert(/for\s*\([^)]+?\)/.test(__helpers.removeJSComments(code)));
这里先用 __helpers.removeJSComments(code) 把学习者提交代码里的注释全部剔除,避免注释中出现单词 "for" 造成误判;随后用正则 /for\s*\([^)]+?\)/ 检查代码中是否存在符合 for (...) 形态的调用。也就是说,仅仅写对结果数组而不写循环,是过不了这一关的——测试强制你真正运用 for 循环。
第二层:校验 myArray 的最终内容
assert.deepEqual(myArray, [1, 2, 3, 4, 5]);
assert.deepEqual 会深度比较提交代码执行后 myArray 的实际值是否与期望值 [1, 2, 3, 4, 5] 完全相等,从而验证循环逻辑是否正确地把 1 到 5 依次推入。
removeJSComments 这类 __helpers 工具在整个课程中被大量复用——仓库中从 Basic JavaScript 到 Workshop 系列的数十个挑战文件(例如 学习金字塔生成器项目中的多道挑战)都采用同样的断言套路:先剥离注释,再以正则或深度比较校验代码结构。这说明 freeCodeCamp 的挑战验证是"语法结构 + 运行结果"双轨制:既要你用了正确的语法结构(for 循环),也要运行结果完全正确([1, 2, 3, 4, 5])。
从挑战自身的元信息还能看出它的运行形态:文档 YAML front matter 中 challengeType: 1 对应的是 JavaScript(JS)挑战类型。在 packages/shared/src/config/challenge-types.ts 中定义了 const js = 1;,而同一文件底部的 viewTypes 映射表(第 106 行起)把 [js]: 'classic',意味着这类挑战会以经典"代码编辑器 + 测试结果"的 classic 视图呈现在学习页面中,学习者边写代码边看测试反馈。
六、隐藏的语法细节:let 与 var、边界条件与死循环风险
let vs var:循环变量的作用域差异
官方示例统一采用 let i = 0,而不是旧式的 var i = 0。两者在 for 循环场景下的关键区别是:
let声明的i具有块级作用域,仅存在于这个 for 语句的范围内,循环结束后访问i会抛出ReferenceError;var声明的i会提升到函数作用域,循环结束后仍可访问到最终值(如上面的例子中i等于 5)。
在涉及闭包(例如循环体内创建函数)时,let 每个迭代都会产生独立的绑定,能避免经典的"var 闭包共享同一变量"陷阱。这也是现代 JavaScript 教程推荐用 let 或 const 声明循环变量的原因。
边界条件:< 与 <= 的一字之差
本课若把条件写成 i < 5 则结果是 0 到 4,写成 i < 6 或 i <= 5 才能覆盖 5。判断边界时记住"条件为 false 就立即停止"这条规则:循环实际执行的最后一次一定是"条件刚好为 true"的那一次。经验法则是:初始化值 + 期望执行次数 与 条件上限要相互配合,例如 for (let i = 1; i < 6; i++) 中的初值 1、上限 6 与期望次数 5 满足 6 - 1 = 5。
死循环风险
如果条件永远为 true(例如忘记写最终表达式 c,且循环体内也不更新计数器),for 循环就会变成死循环,导致浏览器标签页卡死。在挑战环境里编写代码前,建议先在心中推演一遍"计数器是否一定会向使条件为 false 的方向变化"。
七、举一反三:同主题后续挑战里的 for 循环变体
在 basic-javascript.json 结构文件 中紧跟本课的几道挑战,恰好演示了 for 循环三大表达式的可编程性,建议学完本课后立即上手:
1. 用最终表达式调整步长——Iterate Odd Numbers With a For Loop(challenge 文件):通过把最终表达式改为 i += 2,配合奇数初值,一次循环就能收集 1、3、5、7、9:
const ourArray = [];
for (let i = 0; i < 10; i += 2) {
ourArray.push(i);
}
// ourArray 结果为 [0, 2, 4, 6, 8]
2. 让计数器递减——Count Backwards With a For Loop(challenge 文件):把初始化、条件与最终表达式三个方向同时反转(i = 10; i > 0; i--),实现倒序收集。
3. 用循环下标访问数组——Iterate Through an Array with a For Loop(challenge 文件):不再向空数组 push 数字,而是用 i 作为下标读取既有数组元素,即"把 for 循环用于真正遍历数据结构"。
4. 循环套循环——Nesting For Loops(challenge 文件):当一个 for 循环体内再嵌套另一个 for 循环时,内层循环会针对外层每次迭代完整执行一轮,常用于遍历二维数组(矩阵)。
5. 与 while / do...while 横向对比:本课之前学习的是 Iterate with JavaScript While Loops(先判断后执行),本课之后还有 Iterate with JavaScript Do...While Loops(至少执行一次再判断)。三者对比记忆效果最好:
| 循环类型 | 判断时机 | 最少执行次数 | 适用场景 |
|---|---|---|---|
while |
循环体执行前 | 0 次 | 条件已知但次数不定 |
for |
循环体执行前 | 0 次 | 明确知道迭代次数,需计数器 |
do...while |
循环体执行后 | 1 次 | 至少需要执行一次主体 |
总结
"为固定次数运行而设计"的 for 循环,其全部威力都来自三个可选表达式构成的固定执行节奏:初始化只跑一次、条件每轮先判断、最终表达式每轮收尾。通过 freeCodeCamp 这道 "Iterate with JavaScript For Loops" 挑战及其源码,你既掌握了 for (a; b; c) 的结构与时序、push 填充数组的惯用组合、i < 5 这类边界条件的心算方法,也了解了挑战背后的自动测试机制——语法正则与 deepEqual 双校验、let 块级作用域细节,以及它与 while / do...while 的关系。在此基础上继续完成该模块后续的奇数迭代、倒序、数组遍历与循环嵌套练习,就能把 for 循环从"语法"真正内化为"编程直觉"。
进一步查阅
- 本挑战完整源码(含描述、指令、测试与官方解法):cf1111c1c11feddfaeb5bdef.md
- Basic JavaScript 模块结构与课程顺序:basic-javascript.json
- 挑战类型常量与渲染视图映射:challenge-types.ts
- 同一主题的 while 循环前置课程:cf1111c1c11feddfaeb1bdef.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00