freeCodeCamp 基础数据结构教程:用 for...in 遍历 JavaScript 对象的所有键
本篇技术指南以 freeCodeCamp 开源仓库中的一道经典课程关卡 Iterate Through the Keys of an Object with a for...in Statement 为骨架,系统讲解 for...in 循环遍历对象键的原理、与数组索引遍历的差异,并结合关卡内真实出现的 countOnline 练习、测试断言(hints)与官方解(solution),帮助你彻底掌握"遍历对象键 + 方括号动态取值 + 计数器"这一组合套路。读完你将能独立实现此类"按对象键聚合统计"的函数,并理解该知识点在 freeCodeCamp Basic Data Structures 整个课程模块中的承接位置。
一、为什么要遍历对象的键
在 freeCodeCamp 的 Basic Data Structures(基础数据结构)模块中,前几个关卡已经带你完成了对象的增删改查:添加键值对(Add Key-Value Pairs to JavaScript Objects)、修改嵌套对象、用方括号动态访问属性名(Access Property Names with Bracket Notation)、用 delete 删除属性,以及用 hasOwnProperty() / in 检查属性是否存在(Check if an Object has a Property)。
然而以上操作都是针对"单个已知键"展开的。真实场景里你常常不知道对象里到底有哪些键——例如一个聊天系统保存了在线用户集合,每个用户是一个键,对应的值是一个 { online: true/false } 对象。此时你需要一种遍历全部键的手段,这就是本关主角 for...in 的用武之地。
二、for...in 基本语法与运行过程
关卡 description 给出的语法结构如下:
const refrigerator = {
'milk': 1,
'eggs': 12,
};
for (const food in refrigerator) {
console.log(food, refrigerator[food]);
}
运行这段代码会在控制台逐行打印:
milk 1
eggs 12
它的执行过程分三步理解:
- 循环头声明迭代变量:
for (const food in refrigerator)在每次迭代开始时,把food变量设置为对象当前枚举到的那个键(字符串'milk'、'eggs')。 - 循环体读取键名:直接使用
food即可拿到当前键的名字。 - 循环体读取键值:因为
food是运行时才知道的字符串变量,必须使用方括号语法refrigerator[food]才能取得对应的值——这正是上一关卡 Access Property Names with Bracket Notation 强调过的:当属性名是"动态计算出来的"而非字面量时,点号语法不可用,方括号才有效。
换句话说,for...in 的本质是"为对象里的每一个键执行一次循环体",它把"枚举键名"和"按键取值"两条信息都暴露给了你,从而支持对每个键值对做统一处理。
关于对象键顺序的重要说明
关卡中专门给出了一条 NOTE(注意事项),这也是初学者最容易踩坑的地方:
Objects do not maintain an ordering to stored keys like arrays do; thus a key's position on an object, or the relative order in which it appears, is irrelevant when referencing or accessing that key.
翻译过来就是:数组保证元素有确定下标顺序,而对象并不对键维护这种顺序保证。所以你不能依赖 for...in 的迭代顺序去完成依赖"先后次序"的逻辑(例如按顺序拼接字符串或做位置相关计算)。设计算法时,应当把每次循环体迭代视为"处理一个键值对",并保证逻辑与迭代顺序无关。这也是为什么后续关卡 Generate an Array of All Object Keys with Object.keys() 中会特别重申"返回的键数组同样没有特定顺序"。
三、实战关卡:countOnline 在线人数统计
理解了语法后,本关抛出一个贴近真实业务的小任务(instructions):
We've defined a function
countOnlinewhich accepts one argument,allUsers. Use a for...in statement inside this function to loop through theallUsersobject and return the number of users whoseonlineproperty is set totrue.
要求实现一个接收 allUsers 参数的 countOnline 函数,用 for...in 遍历其中每个用户,统计出 online 为 true 的用户个数并返回。题目给出的示例输入是一个"用户 → 状态对象"的映射结构:
{
Alan: {
online: false
},
Jeff: {
online: true
},
Sarah: {
online: false
}
}
它的特点是"两层对象":外层对象的键是用户名,内层每个值又是一个带 online 布尔属性的对象。因此遍历时,allUsers[user].online 才能访问到在线状态。题目同时保证每个用户都会有 online 属性且其值为 true 或 false,所以你不需要做额外的存在性检查——若需要判断属性是否存在,可回看上一关的 hasOwnProperty / in 写法(参考 Check if an Object has a Property 及 Use the delete Keyword to Remove Object Properties)。
关卡初始代码(seed)
以下是关卡预置的起始代码:
const users = {
Alan: {
online: false
},
Jeff: {
online: true
},
Sarah: {
online: false
}
};
function countOnline(allUsers) {
// Only change code below this line
// Only change code above this line
}
console.log(countOnline(users));
学习者只需在 // Only change code below this line 与 // Only change code above this line 之间补全函数体。这种"预置测试数据 + 输出验证 + 限定改动区间"的关卡格式,在仓库的课程文件中被称为 seed-contents,对应数据结构由 curriculum/schema/challenge-schema.js 中的 seed 与 solutions 等字段定义。
四、官方参考解法逐行拆解
关卡 # --solutions-- 段给出的参考实现非常精简且具有代表性:
function countOnline(allUsers) {
let numOnline = 0;
for(const user in allUsers){
if(allUsers[user].online) {
numOnline++;
}
}
return numOnline;
}
其逻辑只有四步:
| 行 | 代码 | 作用 |
|---|---|---|
| 1 | let numOnline = 0; |
初始化计数器,必须在循环外声明,否则每次迭代会被重置 |
| 2 | for(const user in allUsers){ |
用 const 声明循环变量 user,每次迭代绑定当前用户键名 |
| 3 | if(allUsers[user].online) |
通过方括号拿到该用户的状态对象,再读取 online 布尔值 |
| 4 | numOnline++; |
命中即累加,最后在循环结束后 return numOnline |
值得注意的写法要点:
- 用
const声明循环变量:每次迭代user实际上是一个新的绑定,因此const完全合法,也符合"循环内不改键名"的习惯(本关 hint 的正则也同时接受var、let、const三种声明方式)。 - 布尔值直接作为条件:因为题目已保证
online严格为true/false,if(allUsers[user].online)等价于if(allUsers[user].online === true)。 - 返回值在循环体外:
return若写在循环内会在第一次迭代就提前退出,必须放在循环结束后统一返回累计结果。
把该函数放进初始代码运行,会打印 1,因为示例数据中只有 Jeff 在线。若把种子数据替换成 Alan、Sarah 都在线,则输出 2;三人全离线则输出 0。
五、自动化测试如何验证你的实现
freeCodeCamp 关卡内的每一段 hints(测试断言)都会被课程测试运行器执行,作为"通关"判据。本关一共挂了四条断言,第一条约束"实现方式",后三条用三种不同数据验证"输出正确性":
1. 必须真的使用 for...in
assert.match(
__helpers.removeJSComments(code),
/for\s*\(\s*(var|let|const)\s+[a-zA-Z_$]\w*\s+in\s+[a-zA-Z_$]\w*\s*\)/
);
它先把用户代码中的注释移除(__helpers.removeJSComments),再用正则检查是否存在形如 for (var|let|const 变量名 in 对象名) 的写法。这说明本关考核的核心目标就是"用 for...in 这一具体语法",用 Object.keys() 加数组方法或 for...of 即使能算出结果也无法通过本关——它属于下一关卡 Generate an Array of All Object Keys with Object.keys() 的训练范畴。
2~4. 三种输入三种期望输出
| 输入对象 | 在线用户 | 期望返回值 |
|---|---|---|
{ Alan: false, Jeff: true, Sarah: false } |
仅 Jeff | 1 |
{ Alan: true, Jeff: false, Sarah: true } |
Alan、Sarah | 2 |
{ Alan: false, Jeff: false, Sarah: false } |
无 | 0 |
三组数据覆盖了"1 人在线、2 人在线、0 人在线"三类典型场景,防止学习者写出只对单一数据有效的硬编码逻辑。这些断言直接内嵌于关卡 Markdown 的 # --hints-- 区块,与 description、seed、solution 一起构成了一个"可独立执行验证"的完整课程单元;若你在仓库里逐字阅读本关文件 587d7b7d367417b2b2512b1d.md,可以看到 --description--、--instructions--、--hints--、--seed--、--solutions-- 的完整分区结构。
六、for...in 的适用边界与更好替代
需要强调的是,freeCodeCamp 课程在"基础数据结构"阶段刻意先教 for...in,是为了让你理解"对象本质上是键值对集合、可被逐一枚举"这一心智模型。但在现代 JavaScript 工程实践中,需要结合场景选择合适的遍历手段:
- 遍历对象自身的键并逐个取值:
for...in简单直接,但要注意它会连同继承的可枚举属性一起遍历。若只想遍历对象自有属性,通常配合hasOwnProperty判断(参考 Check if an Object has a Property),或直接用Object.keys(obj)拿到自有键数组再遍历(见 Generate an Array of All Object Keys with Object.keys())。 - 只关心对象的值:
Object.values()更直接;同时需要键和值做映射变换:Object.entries()结合for...of或数组方法更常用。 - 遍历数组:
for...of或数组自带方法(forEach/map/filter/reduce)才是正确选择,for...in会把数组下标当作字符串键来枚举,语义并不贴合。
回到本关的计数场景,for...in + 计数器累加的方式,本质上等价于"过滤出满足条件的键并统计个数"——这正是后续学习 filter/reduce 等函数式写法时反复出现的思想雏形。
七、课程定位:本关在 Basic Data Structures 模块中的位置
本关属于 freeCodeCamp 课程体系 JavaScript 认证下的 Basic Data Structures 模块。在该模块的组织文件 curriculum/structure/blocks/basic-data-structures.json 中,它以 id: 587d7b7d367417b2b2512b1d 登记在"对象操作"子序列中,前承"检查对象是否拥有属性"(b1c),后启"用 Object.keys() 生成键数组"(b1e),整体顺序为:数组基础操作 → 数组迭代与多维数组 → 对象增删改查 → 对象键遍历(本关) → 对象键数组化 → 对象内数组修改。这一编排清晰呈现了"从面向已知键的读写,走向面向未知键集合的遍历处理"的教学梯度。
从实现层面看,本关头部元数据标注 challengeType: 1。根据仓库类型定义 packages/shared/src/config/challenge-types.ts,challengeType: 1 对应经典"代码补全 + 测试判定"型关卡,会以 tests 模式运行内置断言来判定学习者是否通关。换言之,你在编辑器中看到的"运行测试 → 通过所有断言 → 完成"的交互,其判定依据就是本文第五节剖析的 # --hints-- 断言集合。
八、小结
本关用一道"在线用户统计"任务,把三个知识点缝合在了一起:
for...in的语法与语义——为对象每个键执行一次循环体,循环变量就是当前键名;- 方括号动态取值——
obj[key]让"运行时才知道的键"可以被安全读取,这是上一关知识的直接复用; - 无顺序保证的枚举心智——对象键不像数组下标那样有序,代码逻辑必须与迭代顺序无关。
写对 countOnline 只是起点,真正重要的是建立"对象是键值对集合,可遍历、可聚合"的数据结构思维。当你继续推进到 Object.keys()、Object.values()、Object.entries() 与现代数组方法时,会发现它们都是对这里 for...in 能力的进一步封装与强化。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00