JavaScript 嵌套对象修改实战:freeCodeCamp Basic Data Structures 之 "Modify an Object Nested Within an Object"
本指南以 freeCodeCamp 开源课程 Basic Data Structures(基础数据结构)模块中编号为 587d7b7c367417b2b2512b19 的经典练习为线索,系统讲解 JavaScript 中对象如何嵌套到任意深度、如何通过点号与方括号表示法读写深层属性,并逐条拆解该挑战的判定规则与官方参考答案。读完你将理解嵌套对象赋值背后的求值链机制,掌握"原地修改深层属性"与"重建对象字面量"两种写法的本质差异,并学会在工程实践中正确选择属性访问语法。
挑战定位:Basic Data Structures 模块中的嵌套对象练习
该挑战文件位于 curriculum/challenges/english/blocks/basic-data-structures/587d7b7c367417b2b2512b19.md,其 frontmatter 定义了本挑战的元信息:
---
id: 587d7b7c367417b2b2512b19
title: Modify an Object Nested Within an Object
challengeType: 1
forumTopicId: 301164
dashedName: modify-an-object-nested-within-an-object
---
各字段含义如下:
id:挑战的唯一标识,同时被用于本地化翻译映射与数据库关联,在 challenge-schema.js 中通过 Joi 校验为 MongoDB ObjectId 格式;challengeType: 1:查询 challenge-types.ts 可知const js = 1,即本挑战属于经典 JavaScript 脚本型练习,会提交一段代码并运行测试断言;dashedName:URL 友好的 slug 形式,校验规则为正则^[a-z0-9-]+$(见 challenge-schema.js 中的slugRE)。
课程组织方面,curriculum/structure/blocks/basic-data-structures.json 中本块声明了 "helpCategory": "JavaScript"、"blockLayout": "legacy-challenge-list",并以 challengeOrder 数组排列了块内全部 20 个挑战。本挑战处于承上启下的位置:前一个练习是 Add Key-Value Pairs to JavaScript Objects(587d7b7c367417b2b2512b18.md,讲解新增顶层键值对),后一个练习是 Access Property Names with Bracket Notation(587d7b7c367417b2b2512b1a.md,讲解方括号访问动态属性名)——本挑战恰好把"新增属性"推进到"修改更深层级上的属性"。
核心概念:对象属性可以嵌套到任意深度
挑战原文(
# --description--节)明确说明:Object properties can be nested to an arbitrary depth, and their values can be any type of data supported by JavaScript, including arrays and even other objects.(对象属性可以嵌套到任意深度,其值可以是 JavaScript 支持的任意数据类型,包括数组甚至其他对象。)
对象是 键值对(key-value pairs) 的集合。键是唯一的属性名(property/key),值则可以是任意数据类型。关键点在于:因为值本身可以是另一个对象,所以对象结构可以形成任意深度的嵌套树。官方给出的示范对象如下:
let nestedObject = {
id: 28802695164,
date: 'December 31, 2016',
data: {
totalUsers: 99,
online: 80,
onlineStatus: {
active: 67,
away: 13,
busy: 8
}
}
};
这个 nestedObject 共有三个顶层属性:
| 属性 | 值类型 | 说明 |
|---|---|---|
id |
number | 数值,28802695164 |
date |
string | 字符串日期 |
data |
object | 嵌套对象,其内部又包含 totalUsers、online 以及更深一层的 onlineStatus 对象 |
注意 data 本身携带两层结构:第一层存放 totalUsers、online 两个统计字段,第二层 onlineStatus 又按 active(活跃)、away(离开)、busy(忙碌)细分了 80 个在线用户的状态。这种"统计信息 → 在线状态明细"的多级组织方式,在真实业务里非常常见——例如仪表盘数据、配置对象、接口响应体(response body)都可能长这样。
尽管结构可能迅速变得复杂,访问所需信息用的仍然是同一种表示法——用点号把每一层键名串成一条"路径"(property path)。
核心操作:给深层属性赋值
若要把 onlineStatus 对象里的 busy 值改为 10,挑战原文给出的写法是:
nestedObject.data.onlineStatus.busy = 10;
这条语句的执行语义可以拆解为两步:
- 读取求值(get):从
nestedObject出发,依次解析.data→.data.onlineStatus→.data.onlineStatus.busy,沿着这条点号链定位到目标属性的引用位置。中间每一步读取的都必须是一个已存在的对象,最终取到的是busy这个槽位(不是它当前的值)。 - 写入赋值(set):把字面量
10存入该槽位。由于对象是按引用传递的,这次修改是原地修改(in-place mutation)——nestedObject.data.onlineStatus依然是原来的那个对象,只是它的busy属性值从8变成了10。
从源码结构可以推断,"任意深度"意味着点号链可以继续延长(例如再嵌套一层 settings.status.details.lastSeen 之类),只要每一层链路上的对象都存在即可。需要注意一个边界情况:如果中间某一层是 undefined(例如 nestedObject.data.onlineStatus 不存在),赋值会抛出 TypeError: Cannot set properties of undefined,因此写入前往往需要保证中间路径对象已存在——这也是本挑战的题目刻意保证结构完整、只需单纯赋值的原因。
动手挑战:将 userActivity 中的 online 改为 45
进入 # --instructions-- 节,题目给出了如下待完成的代码(# --seed-- 节的标准种子内容):
let userActivity = {
id: 23894201352,
date: 'January 1, 2017',
data: {
totalUsers: 51,
online: 42
}
};
// Only change code below this line
// Only change code above this line
console.log(userActivity);
题目要求(原文):
Here we've defined an object
userActivity, which includes another object nested within it. Set the value of theonlinekey to45.
即:把嵌套在 userActivity.data 里的 online 键的值从 42 改为 45。种子代码中"Only change code below this line / above this line"是一对占位注释,提示学习者应在这两行之间插入修改语句,而不是重写整个对象声明。
结合上文示范,答案就是逐层用点号定位后赋值:
userActivity.data.online = 45;
执行后 console.log(userActivity) 输出的 data 部分应为 { totalUsers: 51, online: 45 },其中 totalUsers 保持不变。
通关判定规则逐条拆解
本题共 4 条断言(# --hints-- 节),每条都对应一条 assert 测试字符串,由浏览器端的课程测试环境执行。逐条翻译其含义:
1. userActivity 应保留 id、date、data 三个属性
assert('id' in userActivity && 'date' in userActivity && 'data' in userActivity);
使用 in 操作符检查键是否存在。它强制你不能删除或重命名对象原有顶层键。
2. data 的值应仍是包含 totalUsers 与 online 两个键的对象
assert('totalUsers' in userActivity.data && 'online' in userActivity.data);
进一步约束 data 的内部结构也不能被破坏。
3. data 内层嵌套的 online 属性必须等于 45
assert(userActivity.data.online === 45);
这是本题唯一"结果性"的判定:最终嵌套属性的值必须精确为 45(严格相等)。
4. online 必须用点号或方括号表示法来修改
assert.strictEqual(__helpers.removeJSComments(code).search(/online: 45/), -1);
这是最有趣的一条"过程性"约束。它取出学习者提交的整段源码 code,先调用测试辅助函数 __helpers.removeJSComments(code) 剥离所有注释,再搜索是否出现字面文本 online: 45。断言要求搜索结果严格等于 -1,即源码中不允许出现 online: 45 这样的对象字面量写法。
这条规则的动机很清晰:如果允许直接把种子里的 online: 42 改写成 online: 45,虽然前三条断言也能通过,但学习者就绕开了本题真正想训练的能力——沿属性路径对已有深层属性赋值。而先移除注释,则是防止学习者在注释里写下 online: 45 字样干扰搜索。换言之,即使最终结果正确,只要是用"改字面量"而非"属性赋值"实现的,本题都会判为不通过。
官方参考答案
# --solutions-- 节给出了 freeCodeCamp 官方认可的标准解:
let userActivity = {
id: 23894201352,
date: 'January 1, 2017',
data: {
totalUsers: 51,
online: 42
}
};
userActivity.data.online = 45;
注意官方解答完整保留原始对象声明,仅在其后追加一行赋值语句。这与上一条 hint 的过程约束完全一致:userActivity.data.online = 45; 中并不存在文本 online: 45(这里是 online = 45),故能通过第 4 条断言。这也体现了一种良好的工程习惯:修改应尽量局部化(只动需要改的字段),而不是整块重写数据定义。
方括号版本同样是合法答案:
userActivity['data']['online'] = 45;
点号与方括号:如何选择属性访问语法
尽管本题用点号即可解决,掌握两种表示法的分工是理解整个 Basic Data Structures 对象章节的前提(前导练习 Add Key-Value Pairs to JavaScript Objects 已作铺垫,本块后续的 Access Property Names with Bracket Notation 会进一步强化)。
点号表示法(Dot Notation)
userActivity.data.online = 45;
适用于属性名是合法标识符(以字母、_ 或 $ 开头,不含空格与连字符等)且静态已知的场景。它可读性最好、书写最简洁,是默认首选。
方括号表示法(Bracket Notation)
userActivity['data']['online'] = 45;
// 用变量承载键名(动态键)
const key = 'online';
userActivity.data[key] = 45;
// 键名含空格、连字符或以数字开头等非法标识符字符
someObject['user-name'] = 'bob';
someObject['hair color'] = 'dyed orange';
方括号内接收一个字符串表达式:直接写带引号的字符串键,或写一个值为字符串的变量。在以下场景中方括号是唯一选择:
- 键名包含空格、连字符等非标识符字符;
- 键名是 JavaScript 保留字;
- 键名需要动态计算(来自循环、函数参数或外部数据);
- 需要以数字下标方式混用类数组结构。
结合本挑战主题有一个实用建议:当属性路径既长又含动态片段时(例如 config['feature-flags'][flagName]),可先将路径各段拼进数组再逐层定位,但写入前务必确保各中间层对象已存在,避免触碰 undefined 上的属性。
从源码看:freeCodeCamp 如何"运行"这道题
这道 .md 文件远不止是一篇讲义,而是一个被构建管线解析的"可执行挑战"。仓库中对挑战源的加工逻辑可见于 tools/challenge-parser/parser/plugins(含 add-description、add-seed、add-solution、validate-sections 等插件),它们负责把 # --description--、# --instructions--、# --hints--、# --seed--、# --solutions-- 等分区从 Markdown 中切分出来并转换为前端可渲染、可校验的数据结构。
- 每条 hint 中的
text(如userActivityshould haveid,dateanddataproperties.)用于展示给学习者,testString(如assert(...)片段)则注入运行环境成为真正的测试代码。schema 层面由 challenge-schema.js 中的tests字段约束(testString: Joi.string().allow('').required())。 __helpers.removeJSComments这类工具由课程测试运行环境注入到浏览器或测试 worker 的全局作用域中,供各挑战的testString复用;对全库所有挑战 Markdown 的 schema 与结构校验测试位于 curriculum/src/test/test-challenges.js,其中也使用来自@freecodecamp/shared的challengeTypes与hasNoSolution等常量(js = 1型挑战不在"无解答"集合内,因此本文件带有--solutions--)。
想要在本地复现体验:在
curriculum/下用示例中的种子代码替换到挑战编辑器即可自行运行console.log(userActivity)观察结果;完整的课程构建与测试命令可查看curriculum/package.json提供的脚本。
在课程脉络中的承接关系
本挑战不是孤立的语法点,而是整个对象操作链条的一环。在 basic-data-structures.json 的 challengeOrder 中,围绕对象属性的练习顺序是:
- Add Key-Value Pairs to JavaScript Objects —— 用点号/方括号新增顶层键;
- Modify an Object Nested Within an Object(本文)—— 修改嵌套在对象内的属性;
- Access Property Names with Bracket Notation —— 用方括号 + 变量动态取属性名;
- Use the delete Keyword to Remove Object Properties —— 用
delete删除属性; - Check if an Object has a Property / Iterate Through the Keys of an Object with a for...in Statement / Generate an Array of All Object Keys with Object.keys() —— 对象的查询与遍历;
- Modify an Array Stored in an Object —— 把同套"沿路径修改"的思路迁移到对象内的数组上(先取到数组引用,再对其下标或调用方法操作)。
可见,一旦掌握"逐层求值到槽位、再写入"这一心智模型,后续的"修改对象里的数组"只是把最末层的目标从对象属性换成数组元素而已。
小结
- 对象值可以是任意类型,因此对象可嵌套到任意深度;访问深层属性就是把各层键名用点号(或方括号)串成路径。
- 修改嵌套属性
obj.a.b.c = value是先沿路径求值到目标槽位再做原地赋值;原对象结构不变、仅目标字段更新。 - freeCodeCamp 这道挑战不仅验证最终结果(
online === 45),还通过removeJSComments+ 正则禁止把online改写成对象字面量,强制练习"属性赋值"而非"重写对象"。 - 写代码时应优先点号表示法;键名含特殊字符或需要动态计算时才改用方括号,并注意路径中间层需存在以免抛
TypeError。
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