freeCodeCamp JavaScript 每日编程挑战解析:Challenge 25 "Vowel Repeater" 的题目规则、测试用例与源码级解法
本文以 freeCodeCamp 课程仓库中的 JavaScript 每日编程挑战(Daily Coding Challenge)Challenge 25 "Vowel Repeater" 为主体,完整呈现这道"元音递增重复"题的题目规则、四组官方断言测试、种子代码与参考解法,并结合仓库中的挑战类型定义、课程结构与每日挑战服务源码,讲清楚这道题如何从 Markdown 挑战文件一路走到前端编辑器与测试运行链路。读完后可掌握"计数器 + 单遍扫描"的字符串变换写法、String.repeat 的边界行为,以及 freeCodeCamp 挑战文件的组织规范。
题目定位:每日编程挑战中的第 25 关
该挑战的完整定义位于 68adce01c0e1144d0a902956.md,其 frontmatter 元数据如下:
id: 68adce01c0e1144d0a902956
title: "Challenge 25: Vowel Repeater"
challengeType: 28
dashedName: challenge-25
其中 challengeType: 28 并非随意编号。在共享包 challenge-types.ts 中,28 被定义为 dailyChallengeJs(JavaScript 每日挑战类型,对应 29 为 dailyChallengePy)。同一文件中还可以看到它对前端渲染与提交行为的影响:
viewTypes将dailyChallengeJs映射为'classic',即使用经典代码编辑器视图;submitTypes将其映射为'tests',即完成后通过运行挑战内嵌的assert测试来判定是否通过;- 辅助函数
getDailyCodingChallengeLanguage会把它解析为'javascript'语言。
课程块层面的配置在 daily-coding-challenges-javascript.json 中:本挑战以 id: 68adce01c0e1144d0a902956、标题 "Challenge 25: Vowel Repeater" 登记在 challengeOrder 数组第 25 位(第 105–108 行)。该块还声明了 usesMultifileEditor: true、helpCategory: "JavaScript"、disableLoopProtectTests: true 等属性,其中 disableLoopProtectTests 从源码结构看意味着这类挑战会关闭针对死循环的测试保护机制,属于每日挑战的通用配置。
题目规则:元音出现次序驱动的递增重复
原题的完整规则如下,理解每一条是写出正确实现的前提:
- 递增重复:给定一个字符串,返回一个新字符串,其中每个元音字母比"上一个遇到的元音"多重复一次。第一个元音保持不变;第二个元音连续出现 2 次;第三个元音连续出现 3 次,依此类推。
- 元音范围:
a、e、i、o、u五个字母,大小写均算元音。 - 原始元音保留原大小写:例如首字母为大写的
A,输出中第一个字符仍是A。 - 新增的重复部分一律小写:即"追加"出来的副本统一转小写。
- 非元音字符保持原样:大小写与位置都不变。
规则的关键点在于:重复次数只与"这是第几个元音"有关,与具体是哪个元音无关;且"保持不变"与"翻倍"之间的差别恰好是 1 次追加——这一点对应解法里 count 的初始值设计,下文会详述。
官方测试用例:四组断言完整覆盖边界
挑战文件 # --hints-- 一节给出了四组 assert 测试,这也是前端提交时代码必须全部通过的验收标准:
用例 1:普通小写句子
assert.equal(repeatVowels("hello world"), "helloo wooorld");
用例 2:混合大小写的驼峰词
assert.equal(repeatVowels("freeCodeCamp"), "freeeCooodeeeeCaaaaamp");
用例 3:全大写元音,验证"原始字符保留原大小写 + 追加部分小写"
assert.equal(repeatVowels("AEIOU"), "AEeIiiOoooUuuuu");
用例 4:长句子综合测试,元音计数跨单词连续累计
assert.equal(repeatVowels("I like eating ice cream in Iceland"), "I liikeee eeeeaaaaatiiiiiing iiiiiiiceeeeeeee creeeeeeeeeaaaaaaaaaam iiiiiiiiiiin Iiiiiiiiiiiiceeeeeeeeeeeeelaaaaaaaaaaaaaand");
这四组用例分别覆盖了:基础行为、大小写混合、纯大写元音、以及"元音序号贯穿整个字符串"这一最容易出错的全局计数场景。以用例 3 "AEIOU" 逐字符拆解:
| 字符 | 元音序号 | 输出 | 说明 |
|---|---|---|---|
| A | 第 1 个 | A |
保持不变 |
| E | 第 2 个 | Ee |
原始 E + 1 个小写 e |
| I | 第 3 个 | Iii |
原始 I + 2 个小写 i |
| O | 第 4 个 | Oooo |
原始 O + 3 个小写 o |
| U | 第 5 个 | Uuuuu |
原始 U + 4 个小写 u |
拼接即得 AEeIiiOoooUuuuu,与期望输出一致。再看用例 1 "hello world":元音依次为 e(1)、o(2)、o(3)、o(4),输出为 h e l l o o(空格的 l、o 后追加 1 个 o)与 w o o o r l d(第 4 个元音 o 追加 2 个 o),得到 "helloo wooorld"。
种子代码与挑战文件结构
挑战文件的 # --seed-- / ## --seed-contents-- 一节提供了编辑器中初始显示的模板代码:
function repeatVowels(str) {
return str;
}
这是一个最简桩实现——直接原样返回输入。学习者需要将其改写为真正的转换逻辑。在 freeCodeCamp 的挑战文件规范中,# --description--、# --hints--(内嵌测试)、# --seed--(初始代码)、# --solutions--(参考解答)是标准段落结构,前端会根据 challengeType 决定如何渲染这些段落:对 dailyChallengeJs 类型,编辑器显示 seed 代码,提交时执行 hints 中的测试。
参考解法逐行解析
仓库中 # --solutions-- 给出的参考实现只有 14 行:
function repeatVowels(str) {
const vowels = "aeiouAEIOU";
let count = 0;
let result = "";
for (let char of str) {
result += char;
if (vowels.includes(char)) {
result += char.repeat(count).toLowerCase();
count++;
}
}
return result;
}
逐段拆解其设计:
const vowels = "aeiouAEIOU":把大小写共 10 个字符拼成一个字符串,用String.prototype.includes做成员判断,一行同时满足规则 1(大小写都算元音)。let count = 0的初始值是关键:题目要求"第 1 个元音保持不变",而解法里统一执行char.repeat(count)。当count为 0 时,"x".repeat(0)返回空字符串"",恰好实现"零追加";命中元音后count++自增。于是第 k 个元音追加k-1个副本,总出现次数为 k,与题目"第 2 个出现 2 次、第 3 个出现 3 次"完全对齐。这里count的语义是"已遇到的元音个数",而非"当前元音的序号"。for (let char of str)而非for...in或下标遍历:for...of按码点迭代字符串,对包含 emoji 等多字节字符的输入也安全。result += char先无条件追加原始字符:保证每个字符(含非元音)按原顺序、原大小写进入结果,直接满足规则 3 与规则 5。char.repeat(count).toLowerCase()只作用于追加部分:原始字符char本身未被转换,因此大写元音的首字符保留大写;toLowerCase()只把重复出来的副本变小写,精确实现规则 4。count++只在元音分支内执行:非元音字符不消耗序号,"I like eating ice cream in Iceland" 中跨 6 个单词的元音序号才能连续累加到 11、17、20 等。
整体复杂度为 O(n) 单遍扫描(n 为字符串长度),无额外数据结构,是最直观的线性解。
常见陷阱与边界行为
在实现这道题时,以下几个细节最容易导致测试失败:
repeat的负数陷阱:如果误把count写成"先自增再使用"(即第 1 个元音也追加 1 次),"hello world" 会变成"heello worlold"之类的错误结果;反之若把初始值设为 1 却忘记调整语义,同样会整体偏移 1 次。对照官方用例 1 的helloo(只有 1 个追加o)可以快速自检序号是否正确。toLowerCase作用域:若写成result += char.toLowerCase().repeat(count),原始元音大小写会被破坏,用例 2、3 均会失败。- 元音判断遗漏大写:
vowels若只写"aeiou",用例 3 全大写输入将原样返回,无法通过。 String.repeat的语义:count为 0 返回空串是 ES6 规范行为,也是本解法"第 1 个元音不追加"能成立的语言基础;若传入负数会抛出RangeError,本题中count从 0 递增,不会触发。- 字符串拼接效率:参考解法用
result += char逐字符拼接,教学场景下足够清晰;处理超长文本时可考虑收集到数组再join(""),但对本挑战的输入规模无实际影响。
从挑战文件到运行链路:仓库中的配套实现
理解这道题在仓库中的完整流转,有助于认识 freeCodeCamp 挑战体系的结构:
- 内容定义:挑战正文、测试、seed 与解法全部以 Markdown 形式存放在 daily-coding-challenges-javascript 目录,文件名即挑战
id。课程构建工具(见 challenge-parser 与 curriculum/src 下的get-challenges.ts)会解析这些段落生成结构化数据。 - 块顺序:块结构 JSON 通过
challengeOrder固定 250+ 道挑战的呈现顺序,本挑战排在第 25 位,紧邻 Challenge 24 "Pangram" 之后。 - 类型分发:前端根据 challenge-types.ts 中的
viewTypes/submitTypes映射,选择经典编辑器视图与tests提交模式。 - 每日挑战服务:API 侧提供一组公开只读路由(见 daily-coding-challenge.ts 及其 README 中"仅用于获取挑战信息,提交仍走主 API"的说明),包括按日期
/daily-coding-challenge/date/:date(YYYY-MM-DD)、按月日/day/:day(MM-DD)、/today、/month/:month(YYYY-MM)、/all、/newest六类端点,响应结构由 TypeBox schema(schemas/daily-coding-challenge.ts)约束,其中singleChallengeResponse同时携带javascript与python两种语言的tests数组和challengeFiles。日期解析、"不返回美国中部时间今天之后的未来挑战"等逻辑在 utils/helpers.ts 与路由内实现。 - 前端展示与端到端验证:客户端有专门的每日挑战页面 show-daily-coding-challenge.tsx,配套组件位于 daily-coding-challenge 组件目录,e2e 测试 daily-coding-challenge.spec.ts 覆盖其用户流程。
- 数据种子:tools/daily-challenges 下的
seed-daily-challenges.ts等脚本负责将挑战数据写入数据库的dailyCodingChallenges模型,供上述 API 查询。
小结
Challenge 25 "Vowel Repeater" 是一道用规则约束清晰、测试用例完备的字符串变换题:核心是"全局元音计数 + 单遍扫描 + 条件追加"三步,参考解法利用 String.repeat(0) 返回空串的特性,优雅地把"第一个元音不变"统一进了同一个表达式;大小写规则通过"原始字符原样保留、仅对副本 toLowerCase()"精确落实。仓库中从 Markdown 挑战文件、块结构 JSON、挑战类型映射到 API 只读路由与 e2e 测试的完整链路,则展示了这道题如何被组织、分发与验证。建议读者先在本地按四组断言复现解法,再对照官方用例 4 的长句输出逐字符核对元音序号,以巩固对递增重复逻辑的理解。
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 StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python320
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46567
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20043
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java33951