freeCodeCamp 每日编程挑战 300 实战:用 JavaScript 手写嵌套 Schema 运行时校验器
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇技术指南以 freeCodeCamp 仓库中 Challenge 300: Schema Validator Part 6 为蓝本,讲解如何在无第三方库的前提下,用纯 JavaScript 实现一个能校验「嵌套对象 + 联合类型 + 可选字段 + 字符串数组」结构的运行时数据校验函数。读完你将掌握typeof、Array.isArray、every、includes的组合校验模式,理解 Union(|)、Optional(?)、Array([])三种模式标注语义,并能独立解决同系列全部六道 Schema Validator 题目。
挑战背景:Schema Validator 六部曲的收官之题
Schema Validator 是 freeCodeCamp 每日编程挑战(Daily Coding Challenges)JavaScript 板块中的一组系列题,编号为 Challenge 295 至 Challenge 300。六道题循序渐进地引入数据校验的各个维度:
| 关卡 | 挑战 | 引入的新概念 |
|---|---|---|
| Part 1(Challenge 295) | 单字段 string 校验 | 必填字段、typeof、允许额外键 |
| Part 2(Challenge 296) | 三字段基础类型校验 | string/number/boolean三种基础类型 |
| Part 3(Challenge 297) | 引入 Roles 联合类型 | Union(\|)与枚举值白名单校验 |
| Part 4(Challenge 298) | 可选字段 supporter | Optional(?)字段的「存在才校验」模式 |
| Part 5(Challenge 299) | 字符串数组 badges | Array([])与元素级类型校验 |
| Part 6(Challenge 300) | 嵌套结构 users 数组 | 对象数组的递归校验与空数组语义 |
本关(Part 6)是系列的收官题,将前五关的全部校验技能整合进一个嵌套数据结构:外层对象包裹一个UserProfile对象数组,要求对数组中的每一个元素逐一执行完整校验。
从 daily-coding-challenges-javascript.json 可以确认,本块共编排了 365 道每日挑战(Challenge 1 至 Challenge 365),Schema Validator 系列位于第 295–300 位,紧随其后的是 Challenge 301 "Last Load"。
题目要求:解读 UserProfile 嵌套 Schema
题目给出一个用 JSON 风格书写的 Schema 定义(JavaScript 对象 / Python 字典均适用):
Roles = "user" | "creator" | "moderator" | "staff" | "admin" UserProfile = { username: string, posts: number, verified: boolean, role: Roles, supporter?: boolean, badges: string[] } { users: UserProfile[] }题目同时给出四条标注语义的权威解释,这是正确解题的关键:
|(管道符)表示"或":role字段必须是Roles列表中列出的五个值之一,即"user"、"creator"、"moderator"、"staff"、"admin";?(问号)表示可选:supporter后的问号意味着该字段可以不存在,但只要存在就必须是boolean类型;[]表示数组:UserProfile[]表示由UserProfile对象组成的数组,空数组是合法的(users: []应返回true);- 允许额外键:对象中多出的字段(如测试用例中的
followers: 20)不影响校验结果,无需报错。
值得注意的是,supporter?: boolean的可选语义并不等同于 "允许null"。测试用例中supporter: "false"(字符串)依然会被判为非法,因为可选字段的约束是"要么缺失、要么类型正确"。
十组测试用例:逐条剖析真值表
题目的--hints--节给出了 10 组断言(assert.isTrue/assert.isFalse),它们全面覆盖了合法输入、边界输入与各类非法输入:
| # | 输入特征 | 预期 | 违反的规则 |
|---|---|---|---|
| 1 | 两个合法用户,其一含supporter: true、额外键followers: 20 | true | 全部满足;额外键被允许 |
| 2 | { users: [] } | true | 空数组合法 |
| 3 | users是单个对象而非数组 | false | users必须是数组 |
| 4 | badges: ["liked", 6],混入数字 | false | badges元素必须是 string |
| 5 | supporter: "false"(字符串) | false | 可选字段存在时必须是 boolean |
| 6 | role: "superstar"不在白名单 | false | role必须是五个枚举值之一 |
| 7 | verified: "yes"(字符串) | false | verified必须是 boolean |
| 8 | posts: "10"(字符串) | false | posts必须是 number |
| 9 | 缺少username字段 | false | 必填字段缺失 |
| 10 | 数组第三个元素是空对象{} | false | 每个用户都必须满足全部必填约束 |
这组用例的设计思路非常典型:先验证合法样本与边界样本(空数组、额外键),再用"每种类型各构造一个错误样本"的方式穷举非法路径。第 5–8 号用例分别对应supporter、role、verified、posts四种字段的类型/枚举错误,第 9–10 号用例则聚焦必填字段的缺失问题。
起始代码与解题思路
题目提供的起始代码如下:
function isValidSchema(obj) { return obj; }起始代码只是一个占位实现,直接返回原对象,显然无法通过任何断言。解题的关键在于把校验拆成两层:
- 外层校验:
obj.users必须是一个数组(Array.isArray),因为users: {...}(单个对象)或users缺失都属于非法; - 内层校验:对数组中每个元素执行
isValidUser全字段校验,这本质上就是 Part 5 中isValidSchema的逻辑被抽取成了可复用的子函数。
由于外层要求"每一个用户都合法",数组上现成的every方法是天然的语义对应——every在空数组上恒返回true,恰好契合"空数组合法"的规则,无需额外写空数组特判。
官方解法逐行拆解
题目的--solutions--节给出了完整参考实现:
function isValidSchema(obj) { const roles = ["user", "creator", "moderator", "staff", "admin"]; function isValidUser(user) { return ( typeof user.username === 'string' && typeof user.posts === 'number' && typeof user.verified === 'boolean' && roles.includes(user.role) && (user.supporter === undefined || typeof user.supporter === 'boolean') && Array.isArray(user.badges) && user.badges.every(b => typeof b === 'string') ); } return Array.isArray(obj.users) && obj.users.every(isValidUser); }字段校验的实现细节
1. 必填基础字段:typeof三连username、posts、verified三个必填字段分别用typeof检查'string'、'number'、'boolean'。这里利用了 JS 的一个关键特性:访问不存在的属性返回undefined,而typeof undefined不等于任何目标类型,因此typeof检查天然覆盖了"字段缺失"的情况——这正是第 9、10 号用例被判定为false的原因(user.username为undefined)。
2. 枚举字段:白名单 +includesrole不做类型推导,而是用roles.includes(user.role)做白名单比对。includes使用严格相等(===),因此"superstar"、true、数字等任何非白名单值都会被拒绝。用数组代替switch/多重if的好处是声明式、易扩展,后续想增加角色只需在roles数组中追加一项。
3. 可选字段:undefined短路supporter的校验是这道题最精妙的表达式:
(user.supporter === undefined || typeof user.supporter === 'boolean')它实现了"缺省放行、存在则验型"的双态逻辑:字段缺失时(undefined)直接通过短路返回true;字段存在时继续校验其类型。注意它刻意没有检查null——typeof null === 'object',所以supporter: null会被判定为非法,这与 Schema 语义("指定类型则必须是 boolean")一致。
4. 数组字段:双层校验badges需要两步:先Array.isArray(user.badges)确认它是数组,再user.badges.every(b => typeof b === 'string')确认每个元素都是字符串。第 4 号用例中badges: ["liked", 6]正是被every捕获——数字 6 不满足字符串检查。空数组同样因every的恒真性而通过,与"badges 可为空"的规则吻合。
5. 嵌套结构:every的组合复用外层Array.isArray(obj.users) && obj.users.every(isValidUser)把整道题压缩成一行。users不是数组(第 3 号用例)、数组中存在任一非法用户(第 10 号用例的空对象{})都会让整体返回false。将isValidUser定义为嵌套函数并只负责单个对象,体现了"单一职责"的分解思想,也让系列前五关积累的校验逻辑得以直接复用。
校验技巧纵深:为什么用这些 API
从实现层面看,本解法的每个 API 选择都有明确理由,值得归纳为可迁移的校验模式:
| 模式 | 代码形态 | 适用场景 | 反例陷阱 |
|---|---|---|---|
| 基础类型检查 | typeof x === 'string' | 必填标量字段 | typeof null === 'object',无法区分null与对象 |
| 枚举白名单 | arr.includes(x) | 有限取值集合 | 误用==做宽松比较会放过"10"与10 |
| 可选字段 | x === undefined \|\| 校验(x) | 可选但存在即须验型 | 遗漏undefined判断会把合法缺省判为非法 |
| 数组容器 | Array.isArray(x) | 区分数组与普通对象 | typeof [] === 'object',不能用typeof判断数组 |
| 元素批量校验 | arr.every(pred) | 全元素满足某条件 | some语义相反,会放过混入的非法元素 |
| 缺失字段检测 | 依赖typeof undefined的失败特性 | 必填字段 | 使用if (x)会把0、""、false误判为缺失 |
值得注意的是,typeof user.posts === 'number'会放过NaN、Infinity等特殊数值,typeof user.username === 'string'也会放过空字符串(Part 1 的测试用例明确要求username: ""应返回true)。这说明本系列 Schema 校验的粒度是类型级而非值域级——如果你要校验业务数据的值域约束(如帖子数必须大于 0),需要在typeof之外再叠加范围判断。
用 Python 实现同款 Schema
题目描述明确指出该 Schema 同时适用于 JavaScript 对象与 Python 字典,以下是与官方解法等价的 Python 参考实现(daily-coding-challenges-python块的题目使用dict表述):
ROLES = {"user", "creator", "moderator", "staff", "admin"} def is_valid_schema(obj): def is_valid_user(user): return ( isinstance(user.get("username"), str) and isinstance(user.get("posts"), (int, float)) and not isinstance(user.get("posts"), bool) and isinstance(user.get("verified"), bool) and user.get("role") in ROLES and ("supporter" not in user or isinstance(user["supporter"], bool)) and isinstance(user.get("badges"), list) and all(isinstance(b, str) for b in user["badges"]) ) return isinstance(obj.get("users"), list) and all(is_valid_user(u) for u in obj["users"])Python 版有两个值得注意的差异:一是dict.get()天然返回None,可配合in判断实现可选字段;二是bool是int的子类,若用isinstance(x, int)校验数字字段会放过True/False,因此需要显式排除。这与 JavaScript 中typeof true === 'boolean'的严格区分形成了对照。
在仓库中运行与验证:从题目到线上
挑战文件结构
本关是challengeType: 28的每日编程挑战,其 Markdown 文件遵循 freeCodeCamp 标准挑战格式:frontmatter(id / title / challengeType / dashedName)→--description--(题目描述)→--hints--(测试断言)→--seed--(起始代码)→--solutions--(参考解法)。Chai 的assert.isTrue/assert.isFalse断言会被自由代码营的测试执行器逐一运行,作为判题依据。
每日挑战的发布链路
从仓库源码可以还原出本关从"题目文件"到"每日上线"的完整链路:
- 题目编写:挑战以 Markdown 形式维护在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript 目录下,块的元数据与挑战顺序登记在 daily-coding-challenges-javascript.json;
- 种子数据生成:tools/daily-challenges/seed-daily-challenges.ts 脚本通过 GraphQL 从客户端获取全部 365 道挑战,校验 JavaScript 与 Python 数量一致后,按固定起始日期(
2025-08-11,源码中通过硬编码断言防止发布后误改)每天推进一天,写入 MongoDB 的DailyCodingChallenges集合; - 每日对外提供:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 中的
GET /daily-coding-challenge/date/:date路由按日期从数据库查询当天挑战,且"不会返回晚于美中时区当天日期的挑战",实现每日一题的节奏控制; - 提交与判分:用户完成挑战后通过主挑战提交路由上报,响应 Schema 定义于 api/src/schemas/challenge/daily-coding-challenge-completed.ts,其中
language字段限定为'javascript' | 'python'二选一,与本系列"同一 Schema 双语言实现"的设计一脉相承。
本地调试建议
若想在本地亲手验证解法,无需搭起完整环境,仅用 Node.js 即可:把官方解法与 10 组断言粘贴进一个脚本,用console.assert或直接比较返回值逐条核对。例如最易踩坑的两个用例:
console.log(isValidSchema({ users: [] })); // true(空数组合法) console.log(isValidSchema({ users: {} })); // false(users 必须是数组)总结
Challenge 300 作为 Schema Validator 系列的收官题,把六道题积累的知识点——typeof基础类型校验、白名单枚举、可选字段短路、数组容器与元素级校验——全部整合进嵌套对象数组的校验中。其官方解法的核心价值在于两点:用Array.isArray+every把"对象数组"的递归校验压缩成声明式的两层管道,以及用undefined短路精确表达"可选字段存在才验型"的 Schema 语义。
这套模式在真实工程中同样常见:无论是校验 API 请求体、解析配置文件,还是验证外部数据源的结构,手写轻量校验器都是零依赖环境下最直接的方案。若想继续深入,可依次攻克同系列 Part 1–5 逐步建立能力,或研读仓库中 daily-coding-challenge-validator.ts 等客户端校验实现,观察生产代码如何处理同样的数据结构问题。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考