首页
/ freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Title Case「Capitalize It」

freeCodeCamp 每日编程挑战解析:用 JavaScript 实现 Title Case「Capitalize It」

2026-09-08 11:08:05作者:范垣楠Rhoda

导读

本篇围绕 freeCodeCamp 开源课程中 daily-coding-challenges-javascript(每日编程挑战·JavaScript)区块的第 126 题 Challenge 126: Capitalize Itchallenge 126 原始文档)展开。这道题要求实现一个 titleCase(title) 函数,把任意输入字符串按「每个单词首字母大写、其余字母小写」的标题大小写规则格式化。读完本文,你将掌握该题目的完整约束、四个自动化断言的含义、从种子代码到通过的逐步实现思路、多种可替换写法与边界情况处理,并能理解这类挑战文件(Markdown + Front-matter + --description--/--hints--/--seed--/--solutions--)在 freeCodeCamp 课程体系中是如何被解析、构建与判定的。


一、题目速览:题目在仓库中的位置与文件结构

原始挑战文件位于:

文件以 YAML Front-matter 开头,声明了该挑战的元数据:

---
id: 691b559495c5cb5a37b9b486
title: "Challenge 126: Capitalize It"
challengeType: 28
dashedName: challenge-126
---

各字段含义如下:

字段 含义
id 691b559495c5cb5a37b9b486 全仓库唯一的挑战 ID,同一 ID 也登记在区块清单 daily-coding-challenges-javascript.jsonchallengeOrder 中(该文件中对应的条目是 Challenge 126: Capitalize It
title Challenge 126: Capitalize It 显示标题,按「每日挑战序号 + 题名」命名
challengeType 28 挑战类型编号
dashedName challenge-126 用于 URL / 文件命名的短横线形式

其中 challengeType: 28 值得专门说明。在 packages/shared/src/config/challenge-types.ts 中,类型常量被集中定义:

const dailyChallengeJs = 28;
const dailyChallengePy = 29;

也就是说,28 对应的是 JavaScript 每日编程挑战dailyChallengeJs),29 则对应 Python 版本。同文件还给出了该类型的两条关键映射:

// 挑战编辑器视图类型:classic(经典代码挑战)
[dailyChallengeJs]: 'classic',

// 提交方式:tests(由测试用例自动判定)
[dailyChallengeJs]: 'tests',

这解释了为什么本文这道题使用「代码 + 断言」的形式:它属于 classic 视图,代码运行后由平台执行 --hints-- 中的断言进行判定。


二、需求拆解:三道 Title Case 规则

原始文档的 --description-- 部分是本题的完整需求描述,原文如下:

Given a string title, return a new string formatted in title case using the following rules:

  • Capitalize the first letter of each word.
  • Make all other letters in each word lowercase.
  • Words are always separated by a single space.

翻译并拆解为三条明确规则:

  1. 每个单词的首字母大写:无论输入中该词是 helloHello 还是 HELLO,输出时首字母一律为大写字母 H
  2. 单词内其余字母全部小写:例如 JAVASCRIPT 输出时应为 Javascript,而不是保持全大写。
  3. 单词之间始终以单个空格分隔:输入本身保证单词之间只有一个空格。这意味着实现时无需考虑 " "(连续空格)、制表符、换行等分隔符的鲁棒性,可以直接放心地以 " " 作为唯一分隔依据——这是一个重要的前提约定,直接影响了下面「用 split(" ")」这一做法是否安全。

这三个条件合在一起,定义了标准的英文「标题大小写」(Title Case)语义:每个词只保留一个首字母大写,其余一律小写


三、测试断言:四个输入输出的含义

文档的 --hints-- 区块给出了四个自动化断言,它们以 assert.equal(...) 的形式写在 JS 代码块中,由课程判定器在用户代码运行后执行。逐个分析:

assert.equal(titleCase("hello world"), "Hello World");

覆盖「全小写输入」hello world 中每个单词都全小写,只需把每个词的首字母大写。这是最基础的一档。

assert.equal(titleCase("the quick brown fox"), "The Quick Brown Fox");

覆盖「多个单词、全小写输入」:四个单词逐一处理,验证对单词数量没有写死、循环处理正确。

assert.equal(titleCase("JAVASCRIPT AND PYTHON"), "Javascript And Python");

覆盖「全大写输入」:这是最容易出错的一档——只把首字母大写是不够的,其余字母必须被主动转小写,否则 JAVASCRIPT 会错误地原样保留。

assert.equal(titleCase("AvOcAdO tOAst fOr brEAkfAst"), "Avocado Toast For Breakfast");

覆盖「大小写混杂输入」AvOcAdO 这类词内部大小写完全随机,输出必须被"归一化"为 Avocado。此断言检验的是规则 2(其余字母全部小写)与规则 1 的组合效果,是本题含金量最高的用例。

四个用例的语义可以汇总成一张表:

输入 期望输出 主要验证点
"hello world" "Hello World" 基本首字母大写
"the quick brown fox" "The Quick Brown Fox" 多单词逐个处理
"JAVASCRIPT AND PYTHON" "Javascript And Python" 全大写需转小写
"AvOcAdO tOAst fOr brEAkfAst" "Avocado Toast For Breakfast" 混杂大小写归一化

顺带一提,把 titleCase 用在第四档输入上、由它返回完全规范化的 "Avocado Toast For Breakfast",本身就是对大小写归一化能力的一次完整检验,可作为你本地自测时最关键的回归用例。


四、种子代码:为什么它「通过不了测试」

文档的 --seed----seed-contents-- 提供了初始代码:

function titleCase(title) {
  return title;
}

这是一个最小骨架:函数已声明、参数已就位、返回值类型正确(字符串),但它原样返回输入,不做任何转换。对照测试:

  • titleCase("hello world") 返回 "hello world",而期望是 "Hello World",断言失败。

种子代码的作用是让学习者明确「函数签名已经定好、不要改动,只需填充函数体」,这与 freeCodeCamp 经典代码挑战的通用模式一致:学习者面对的是半成品函数,需要补齐逻辑让 --hints-- 中的断言全部通过。


五、逐步实现:Split → Map → Join 三段式解法

5.1 算法设计:把「整串处理」降维成「逐词处理」

观察三规则可以发现,规则 1 和规则 2 都是针对单个单词内部的变换,只有规则 3 描述了单词之间的分隔关系。因此自然的设计思路是:

  1. split(" ") 依据单个空格把字符串切成单词数组(规则 3 保证分隔符唯一,切割是确定的);
  2. 每一个单词执行同一套大小写变换:首字母大写 + 其余小写;
  3. join(" ") 把处理完的单词按单个空格重新拼回字符串(保持分隔符不变)。

这样就把「对整个字符串做大小写变换」这个较难一次到位的问题,降维成「对每个单词做统一变换」,逻辑单一、易于验证。

5.2 参考实现与逐行讲解

文档 --solutions-- 区块给出的参考实现如下:

function titleCase(title) {
  return title
    .split(" ")
    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
    .join(" ");
}

逐段解读这条链式调用

  1. title.split(" ") —— 以单个空格切分。结合规则 3,这里不传正则而是传普通字符串 " ",是安全且可读的写法;同时切分发生在 .map 之前,先产生了数组中间结果。
  2. .map(word => ...) —— 对每个词执行回调,返回新数组。不修改原字符串或原数组,体现函数式、无副作用风格(也正因为 split/map/join 都返回新值,链式写法才能成立)。
  3. word.charAt(0).toUpperCase() —— 取首字符并转大写。这里选用 charAt(0) 而非 word[0] 的原因在 5.3 详述;当词首是字母(数字、标点如 "1st""'")时,toUpperCase() 对该字符是幂等操作,因此这条写法对「首字符不是字母」的边界同样安全。
  4. word.slice(1).toLowerCase() —— 取第 2 个字符起的所有剩余字符并整体转小写。若单词只有 1 个字符,slice(1) 返回空字符串 "",拼接结果恰好是「单字母小写后的本身」,符合规则 2。
  5. 最后用 "+" 把「大写首字符」与「小写剩余部分」拼成完整单词,经 .join(" ") 还原为标题串。

对整个算法做复杂度分析:splitmapjoin 各遍历一次字符/单词,总时间复杂度为 O(n)(n 为字符串长度),且只需常数级额外空间,属于对该问题的最优线性解。

5.3 为什么用 charAt(0) 而不是 word[0]

word[0] 在 ES5+ 中也能取到首字符,但存在一个历史遗留缺陷:当索引位置不存在时,word[0] 返回 undefined,而 undefined.toUpperCase() 会抛出 TypeError。对本文场景,若 title 是空字符串,"".split(" ") 得到 [""]map 回调中的 ""[0] 即为 undefined,直接崩溃;而 "".charAt(0) 返回 """".toUpperCase()"""".slice(1)"",拼接后输出 "",优雅地处理了空输入。因此在面向「边界用例可能出现在隐藏断言中」的课程环境里,参考实现特意选用 charAt(0) 是更稳健的选择。

5.4 可以替换的等价写法

以下写法在语义上等价,可作为学习对照(均为我们基于题面推导的替代实现,不是仓库原文档内容):

// 写法 A:用 String.prototype.slice 取首字符
function titleCaseA(title) {
  return title
    .split(" ")
    .map(word => word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase())
    .join(" ");
}

// 写法 B:用正则一次性匹配并替换每个单词
function titleCaseB(title) {
  return title.replace(/\b\w+\b/g, word =>
    word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
  );
}

// 写法 C:逐词遍历 + 累积
function titleCaseC(title) {
  const words = title.split(" ");
  const result = [];
  for (const word of words) {
    result.push(word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
  }
  return result.join(" ");
}

几点取舍说明:

  • 写法 B 的 \b 单词边界基于正则 \w(字母数字下划线),且对输入中连续多个空格也能容错——但本题已保证单词间单空格,所以该优势用不上,反而正则可读性略差;
  • 写法 C 用显式循环与数组累积,更贴近命令式风格,便于初学者打断点调试;
  • 三种写法都会把 "JAVASCRIPT AND PYTHON" 正确转为 "Javascript And Python",因为关键动作都是「首大写 + 余小写」。

提交前建议在自己的 Node.js 环境(或浏览器开发者工具控制台)里把四个断言原样跑一遍:

console.log(titleCase("hello world") === "Hello World"); // true
console.log(titleCase("the quick brown fox") === "The Quick Brown Fox"); // true
console.log(titleCase("JAVASCRIPT AND PYTHON") === "Javascript And Python"); // true
console.log(titleCase("AvOcAdO tOAst fOr brEAkfAst") === "Avocado Toast For Breakfast"); // true

全部输出 true,即可确认与判定器的期望一致。


六、常见误区与边界情况清单

把本题的易错点整理如下,可作为自测对照表:

场景 示例 常见错误 正确结果 对策
全大写输入 "JAVASCRIPT" 只做大写首字母,输出 "JAVASCRIPT" "Javascript" 必须 slice(1).toLowerCase()
混杂大小写 "AvOcAdO" 只把首字母转大写 "Avocado" 首大写之外,剩余统一小写
空字符串 "" titleCase("") word[0] 取到 undefined 抛错 "" 使用 charAt(0)
单字符单词 "a b" slice(1) 误写为 slice(0) "A B" slice(1) 对单字符返回空串,拼接正确
含非首字母数字的词 "3d model" 误判首字符需要变换 "3d Model" toUpperCase() 对非字母幂等,"3d" 仍保持小写

需要再次强调的是:因为规则 3 明确规定「单词之间始终只有单个空格」,本实现不需要处理 " hello world" 这类多空格输入,也不需要调用 trim()。如果脱离了这道题的约定去推广到通用标题转换工具,则要另行考虑分词与去空白策略——这是把「做对题」升级为「做好工具」时需要补充的健壮性设计,而不是本题的隐藏要求。


七、在课程体系中的位置:challengeType 28 背后的源码链路

本挑战并不仅仅是孤立的算法练习,它在 freeCodeCamp 仓库内有一条完整的源码链路,理解它有助于你摸清「一份 Markdown 是如何变成一道可判定题目的」。

7.1 区块登记与关卡排序

本题 ID 691b559495c5cb5a37b9b486 被登记在区块结构中:curriculum/structure/blocks/daily-coding-challenges-javascript.json。该 JSON 声明了区块的若干行为属性:

{
  "isUpcomingChange": true,
  "usesMultifileEditor": true,
  "helpCategory": "JavaScript",
  "disableLoopProtectTests": true,
  "blockLayout": "legacy-challenge-list",
  "challengeOrder": [ ... ]
}

其中:

  • isUpcomingChange: true 表示该区块属于即将上线的课程。对应地,在课程工具链中,curriculum/src/config.ts 通过环境变量 SHOW_UPCOMING_CHANGES(值为字符串 'true' 时生效)控制是否把这类区块纳入渲染范围;
  • challengeOrder 数组以 ID + 标题的形式维护挑战的先后顺序,本挑战是数组中的第 126 条(编号从 Challenge 1 起连续递增);
  • 相邻题目也可互为延伸参照:例如 Challenge 37「Sentence Capitalizer」与 Challenge 38「Slug Generator」同样涉及字符串大小写/文本转换,学完本题后可顺路练习。

7.2 挑战类型到构建器的映射

在构建侧,packages/challenge-builder/src/build.ts 维护了一张「挑战类型 → 构建函数」的分派表:

[challengeTypes.dailyChallengeJs]: buildJSChallenge,

challengeTypes.dailyChallengeJs 的值即 28,因此本文件被分派到 buildJSChallenge——与普通 js 类型共用同一套 JS 挑战构建逻辑。同时,在前端视图层,packages/shared/src/config/challenge-types.ts 将 28 映射为 'classic' 视图、'tests' 提交类型,与第六节前段所述的判定流程一一对应。

7.3 每日挑战的呈现入口

在客户端,每日挑战有专门的呈现组件,例如 client/src/components/daily-coding-challenge/widget.tsx 提供「今日挑战」入口,并跳转到 /learn/daily-coding-challenge/... 路由;client/src/utils/daily-coding-challenge-validator.ts 则给出了数据库侧每日挑战记录的结构校验(含 challengeNumbertitledescription、按 javascript/python 分语言的 testschallengeFiles)。这说明「Capitalize It」所属的 JS 每日挑战题源,在数据模型上最终会被组织为按日期/编号排布的、同时含 JS 与 Python 两个语言版本测试与骨架文件的记录——第 28 号类型正是这套体系中「JavaScript 一侧」的标识。


八、小结

Challenge 126「Capitalize It」是一道非常适合入门的字符串处理题:题面用三条规则把「标题大小写」定义得清晰无歧义,四个断言从「全小写、多单词、全大写、混杂大小写」四个角度覆盖了实现必须处理的全部输入形态。参考解法 split(" ")map(首字符大写 + 余部小写)join(" ") 结构简洁、线性复杂度、无副作用,并通过对 charAt(0) 的选用天然规避了空输入崩溃。

从这道题出发,你既可以把实现替换为正则或命令式循环来巩固 JavaScript 数组与字符串 API,也可以顺着 daily-coding-challenges-javascript.jsonchallengeOrder 继续刷同区块的其他题目;同时,理解 challengeType: 28challenge-types.tsbuild.ts 中的映射,也能让你对 freeCodeCamp「一份 Markdown 挑战文件如何被解析、构建并按测试自动判定」的完整流水线建立直观认识。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390