news 2026/9/10 16:57:08

freeCodeCamp 每日编程挑战 300 实战:用 JavaScript 手写嵌套 Schema 运行时校验器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
freeCodeCamp 每日编程挑战 300 实战:用 JavaScript 手写嵌套 Schema 运行时校验器

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 实现一个能校验「嵌套对象 + 联合类型 + 可选字段 + 字符串数组」结构的运行时数据校验函数。读完你将掌握typeofArray.isArrayeveryincludes的组合校验模式,理解 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)可选字段 supporterOptional(?)字段的「存在才校验」模式
Part 5(Challenge 299)字符串数组 badgesArray([])与元素级类型校验
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: 20true全部满足;额外键被允许
2{ users: [] }true空数组合法
3users单个对象而非数组falseusers必须是数组
4badges: ["liked", 6],混入数字falsebadges元素必须是 string
5supporter: "false"(字符串)false可选字段存在时必须是 boolean
6role: "superstar"不在白名单falserole必须是五个枚举值之一
7verified: "yes"(字符串)falseverified必须是 boolean
8posts: "10"(字符串)falseposts必须是 number
9缺少username字段false必填字段缺失
10数组第三个元素是空对象{}false每个用户都必须满足全部必填约束

这组用例的设计思路非常典型:先验证合法样本与边界样本(空数组、额外键),再用"每种类型各构造一个错误样本"的方式穷举非法路径。第 5–8 号用例分别对应supporterroleverifiedposts四种字段的类型/枚举错误,第 9–10 号用例则聚焦必填字段的缺失问题。

起始代码与解题思路

题目提供的起始代码如下:

function isValidSchema(obj) { return obj; }

起始代码只是一个占位实现,直接返回原对象,显然无法通过任何断言。解题的关键在于把校验拆成两层:

  1. 外层校验obj.users必须是一个数组(Array.isArray),因为users: {...}(单个对象)或users缺失都属于非法;
  2. 内层校验:对数组中每个元素执行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三连usernamepostsverified三个必填字段分别用typeof检查'string''number''boolean'。这里利用了 JS 的一个关键特性:访问不存在的属性返回undefined,而typeof undefined不等于任何目标类型,因此typeof检查天然覆盖了"字段缺失"的情况——这正是第 9、10 号用例被判定为false的原因(user.usernameundefined)。

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'会放过NaNInfinity等特殊数值,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判断实现可选字段;二是boolint的子类,若用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断言会被自由代码营的测试执行器逐一运行,作为判题依据。

每日挑战的发布链路

从仓库源码可以还原出本关从"题目文件"到"每日上线"的完整链路:

  1. 题目编写:挑战以 Markdown 形式维护在 curriculum/challenges/english/blocks/daily-coding-challenges-javascript 目录下,块的元数据与挑战顺序登记在 daily-coding-challenges-javascript.json;
  2. 种子数据生成:tools/daily-challenges/seed-daily-challenges.ts 脚本通过 GraphQL 从客户端获取全部 365 道挑战,校验 JavaScript 与 Python 数量一致后,按固定起始日期(2025-08-11,源码中通过硬编码断言防止发布后误改)每天推进一天,写入 MongoDB 的DailyCodingChallenges集合;
  3. 每日对外提供:api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 中的GET /daily-coding-challenge/date/:date路由按日期从数据库查询当天挑战,且"不会返回晚于美中时区当天日期的挑战",实现每日一题的节奏控制;
  4. 提交与判分:用户完成挑战后通过主挑战提交路由上报,响应 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 16:56:43

ToolJet 快速体验指南:用 Docker 一键启动与自定义端口部署

ToolJet 快速体验指南:用 Docker 一键启动与自定义端口部署 【免费下载链接】ToolJet Open-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build v…

作者头像 李华
网站建设 2026/9/10 16:52:19

用环境变量驱动极简导航页:华为开发者空间部署envlinks实战

1. 项目背景与方案选型解析1.1 为什么需要一个“极简导航页”先聊聊我做这个事的初衷。平时工作台上一堆服务:Git仓库、文档站、监控面板、NAS后台、路由器管理页、各种内部系统……浏览器书签栏早就塞满了,每次要找某个地址得翻半天,还不一定…

作者头像 李华
网站建设 2026/9/10 16:52:16

JAVA毕设项目:基于 Web 的实验室耗材库存管理系统的设计与实现 基于 Web 架构的实验室耗材全生命周期管理平台 (源码+文档,讲解、调试运行,定制等)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

作者头像 李华
网站建设 2026/9/10 16:52:13

2026直播导播软件横评:vMix、OBS与DingCaster三国杀

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华