freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Schema Validator(Challenge 295 实战指南)
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
导读
本文围绕 freeCodeCamp 课程仓库中 Daily Coding Challenge(每日编程挑战)模块的Challenge 295: Schema Validator Part 1(curriculum/challenges/english/blocks/daily-coding-challenges-javascript/6a0dcc730cb92a616f86f0bf.md)展开,完整讲解"如何用typeof校验一个对象是否匹配{ username: string }这样的 JSON Schema"这道经典入门的全部细节。读完本文,你不仅能掌握该题的题意、测试用例、种子代码与参考实现,还能顺着同系列 Part 2 ~ Part 6 的演进脉络,理解枚举、可选字段、数组元素与嵌套对象等 Schema 校验的进阶写法,并了解 freeCodeCamp 平台侧如何用 Joi 校验每日挑战数据。
一、挑战背景:Daily Coding Challenge 与 Schema Validator 系列
在 freeCodeCamp 的课程结构中,每日编程挑战是一个独立的 JavaScript 块(block),其块元数据定义在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中。该块共包含365 道挑战,从Challenge 1: Vowel Balance到Challenge 365: The Last Challenge: Bucket Fill 3。Challenge 295 正位于这一长列表的中后段(该 JSON 的第 1186~1188 行),与 Challenge 296~300 共同组成了一个六连发的Schema Validator(Schema 校验器)专题:
| 挑战编号 | 文件 ID | 主题 |
|---|---|---|
| Challenge 295 | 6a0dcc730cb92a616f86f0bf | Schema Validator Part 1(单字段username: string) |
| Challenge 296 | 6a0dcc730cb92a616f86f0c0 | Schema Validator Part 2(多字段:posts、verified) |
| Challenge 297 | 6a0dcc730cb92a616f86f0c1 | Schema Validator Part 3(枚举类型Roles) |
| Challenge 298 | 6a0dcc730cb92a616f86f0c2 | Schema Validator Part 4(可选字段supporter?) |
| Challenge 299 | 6a0dcc730cb92a616f86f0c3 | Schema Validator Part 5(字符串数组badges: string[]) |
| Challenge 300 | 6a0dcc730cb92a616f86f0c4 | Schema Validator Part 6(嵌套对象数组users: UserProfile[]) |
Challenge 295 作为该专题的第一题,只要求校验一个字段,是理解后续所有复杂 Schema 的基石。从块的配置("isUpcomingChange": true、"helpCategory": "JavaScript"、"usesMultifileEditor": true、"blockLayout": "legacy-challenge-list")可以看出,该块属于仍在演进中的课程内容,并使用多文件编辑器布局。
二、任务说明:题目与 Schema 定义
题目的描述(--description--)非常简洁:给定一个 JavaScript 对象(Python 中为字典),判断它是否匹配如下 Schema:
{ username: string }并附带一条关键规则:
- Extra keys are allowed(允许出现额外的键)
这两点共同界定了本题的判定语义:
- Schema 校验是"白名单式"检查:只要对象中
username的值是字符串类型,就视为匹配,而无需关心对象里是否还藏着posts、followers等其他键; - 题目采用 JSON Schema 的简化记法——键名后跟类型名,
string表示该字段的值必须是字符串。
与真实 Schema 校验的对应关系
本题的场景在真实工程中极为常见:例如校验 API 请求体、校验用户配置对象、校验第三方回调数据等。简化后的{ username: string }等价于 JSON Schema 中的:
{ "type": "object", "properties": { "username": { "type": "string" } }, "required": ["username"], "additionalProperties": true }其中required保证username必须存在,additionalProperties: true对应"Extra keys are allowed"。理解了这层对应关系,后续学习 JSON Schema 标准或 Zod、Joi 等校验库时就能快速迁移。
三、测试用例(Hints)逐条解析
题目通过--hints--提供了 5 个测试断言(使用 Chai 的assert.isTrue/assert.isFalse),它们精确刻画了匹配规则。逐一分析:
| # | 测试输入 | 期望结果 | 判定依据 |
|---|---|---|---|
| 1 | { username: "bob" } | true | username是字符串,且无其他字段,匹配 |
| 2 | { username: "jen", posts: 30 } | true | username是字符串,posts属于"额外键",允许存在 |
| 3 | { username: "" } | true | 空字符串依然是字符串类型,匹配(不要求非空) |
| 4 | { username: 7 } | false | 数字7不是字符串,类型不符 |
| 5 | { posts: 25 } | false | 缺少必需字段username |
assert.isTrue(isValidSchema({ username: "bob" })); assert.isTrue(isValidSchema({ username: "jen", posts: 30 })); assert.isTrue(isValidSchema({ username: "" })); assert.isFalse(isValidSchema({ username: 7 })); assert.isFalse(isValidSchema({ posts: 25 }));这组用例刻意覆盖了三个边界:
- 额外键不干扰判定(用例 2),这是"Extra keys are allowed"的直接体现;
- 空字符串是合法字符串(用例 3),Schema 只约束类型、不约束内容;
- 缺少必需字段必须判为不匹配(用例 5),说明
username是必填字段,仅类型对但键不存在也不行。
四、种子代码与解题思路
题目的种子代码(--seed--/--seed-contents--)给出了函数骨架:
function isValidSchema(obj) { return obj; }初始实现直接返回了入参对象obj,这显然无法通过任何断言(对象本身既不是true也不是false)。解题的核心思路可以拆成三步:
- 访问目标字段:通过
obj.username读取对象的username属性; - 进行类型判定:用
typeof obj.username判断其类型是否为'string'; - 返回布尔值:
typeof的结果是字符串,与'string'比较后得到布尔结果。
关于typeof运算符,有几个关键行为必须了解(这也是整个 Schema Validator 系列反复使用它的原因):
typeof对基本类型的判定:typeof "bob" === "string"、typeof 7 === "number"、typeof true === "boolean";- 当属性不存在时,
obj.username的值为undefined,而typeof undefined === "undefined",它不等于'string',因此表达式会得到false——这正是用例 5(缺少username)能被正确判为false的底层原因; - 无需额外判空:即使对象本身为空
{},typeof {}.username也是'undefined',比较结果依旧安全地返回false。
五、参考实现(官方 Solution)
题目的--solutions--部分给出了最精简且完全满足所有断言的参考实现:
function isValidSchema(obj) { return typeof obj.username === 'string'; }一行代码完成整个校验。其正确性可以对照全部 5 个断言逐一验证:
isValidSchema({ username: "bob" })→typeof "bob" === 'string'→trueisValidSchema({ username: "jen", posts: 30 })→ 只看username→trueisValidSchema({ username: "" })→typeof "" === 'string'→trueisValidSchema({ username: 7 })→typeof 7 === 'number'→falseisValidSchema({ posts: 25 })→typeof undefined === 'undefined'→false
为什么要用typeof而不是其他方式
新手常见的错误是用obj.username的真值判断(如if (obj.username)),或直接比较obj.username === "string",两者都会失败:
- 真值判断会把
username: ""判为false(与用例 3 冲突); - 直接与
"string"比较是把值本身和类型名混淆,永远为false; - 而
typeof是 JavaScript 内置的类型检测运算符,能正确识别原始类型,是这类 Schema 校验的标准工具。
六、系列演进:从单字段到嵌套对象(Part 1 → Part 6)
Challenge 295 的难度被刻意控制在"单字段类型校验",后续五道题则在它之上逐层叠加规则。把整套系列连起来读,就是一份循序渐进的对象 Schema 校验教程:
Part 2(Challenge 296):多字段与多类型
Schema 扩展为三个字段,要求所有字段同时满足:
{ username: string, posts: number, verified: boolean }参考实现用&&串联三个typeof判断:
function isValidSchema(obj) { return ( typeof obj.username === 'string' && typeof obj.posts === 'number' && typeof obj.verified === 'boolean' ); }其中posts: "21"(字符串化的数字)会被判为false,体现了类型必须精确匹配、不做隐式转换的原则。
Part 3(Challenge 297):枚举类型Roles
新增role: Roles字段,Roles用管道符|表示"或":
Roles = "user" | "creator" | "moderator" | "staff" | "admin" { username: string, posts: number, verified: boolean, role: Roles }实现上先用数组保存合法取值,再用Array.prototype.includes判断:
const roles = ["user", "creator", "moderator", "staff", "admin"]; return ( typeof obj.username === 'string' && typeof obj.posts === 'number' && typeof obj.verified === 'boolean' && roles.includes(obj.role) );role: "guest"(不在枚举内)、role: true(类型错误)都会返回false。
Part 4(Challenge 298):可选字段supporter?
?表示字段可选,但一旦存在就必须是指定类型:
supporter?: boolean参考实现用短路判断处理"未定义或为布尔":
(obj.supporter === undefined || typeof obj.supporter === 'boolean')注意supporter: "true"(字符串)仍会判为false——可选不等于宽松。
Part 5(Challenge 299):字符串数组badges: string[]
[]表示字段应为字符串数组(可为空数组):
badges: string[]实现要点是Array.isArray加上every逐个元素检查类型:
Array.isArray(obj.badges) && obj.badges.every(b => typeof b === 'string')badges: []合法(空数组通过every),badges: ["first-post", 18]非法(混入数字)。
Part 6(Challenge 300):嵌套对象数组users: UserProfile[]
这是系列的收官题,将前面定义的全部规则封装成UserProfile类型,再要求users是该类型的对象数组:
UserProfile = { username: string, posts: number, verified: boolean, role: Roles, supporter?: boolean, badges: string[] } { users: UserProfile[] }参考实现把字段校验逻辑提取为内部函数isValidUser,再配合Array.isArray与every:
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); }users: []合法;users是单个对象而非数组、数组中混入数字徽章、缺少username、role不在枚举内等情况均判为false。这种"递归复用校验函数"的模式,与真实项目中 Zod / Joi / TypeScript 类型体操处理嵌套结构的手法一脉相承。
七、平台侧实现:挑战数据如何被校验
作为 freeCodeCamp 开源仓库的一部分,Daily Coding Challenge 不只是静态 Markdown 文件,它在运行时还有完整的平台支撑:
- API 服务:
api/src/daily-coding-challenge/目录(routes、schemas、utils 与 README.md)提供获取每日挑战信息的端点,README 明确指出"获取每日挑战信息的端点;每日挑战提交仍位于 API 主体部分"; - 前端数据校验:客户端在 client/src/utils/daily-coding-challenge-validator.ts 中用Joi定义并校验从数据库取回的挑战数据结构——要求
id、challengeNumber(正整数且 ≥1)、title、date、description以及javascript/python两个语言版本(各自包含tests数组与challengeFiles数组)都必须存在。这与本题"用代码校验对象是否匹配 Schema"的练习思路形成呼应:平台在幕后同样在做 Schema 校验,只是换成了工业级校验库; - 挑战类型:本挑战的 frontmatter 声明
challengeType: 28,标识其为每日编程挑战类型;块配置中的disableLoopProtectTests: true表示测试不受循环保护限制,方便编写涉及迭代的测试。
八、动手验证:本地快速测试你的实现
无需启动整个平台,你也可以在本地 Node.js 环境验证答案。将题目中的断言转换成一段自测脚本:
function isValidSchema(obj) { return typeof obj.username === 'string'; } const tests = [ [{ username: "bob" }, true], [{ username: "jen", posts: 30 }, true], [{ username: "" }, true], [{ username: 7 }, false], [{ posts: 25 }, false] ]; tests.forEach(([input, expected], i) => { const result = isValidSchema(input); console.log(`Test ${i + 1}: ${result === expected ? 'PASS' : 'FAIL'} (got ${result}, expected ${expected})`); });运行node test.js后应看到 5 个PASS。你可以在此基础上自行扩展边界用例,例如null、undefined、字符串包装对象new String("bob")(注意typeof new String("bob")的结果是'object',会判为false),加深对typeof局限性的理解。
九、小结
Challenge 295 用一道极简的题目,讲透了对象 Schema 校验的三大核心原则:按字段检查、用typeof判类型、允许额外键。以它为起点,Part 2~6 依次引入了多字段、枚举、可选字段、数组元素与嵌套对象,构成了一条完整的 Schema 校验学习路径。而在 freeCodeCamp 的工程实现中,同样的思路被 Joi(前端挑战数据校验)与 API 服务(每日挑战信息分发)所承接——这正是"课程练习"与"工程实战"相互印证的典型案例。
后续可以继续深入阅读仓库中的相邻挑战文件(如 Challenge 296、Challenge 297 至 Challenge 300),或对比 client/src/utils/daily-coding-challenge-validator.ts 中 Joi 版校验写法,体会手写校验与声明式校验库的异同。
【免费下载链接】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),仅供参考