freeCodeCamp Basic JavaScript 挑战解析:Use Recursion to Create a Range of Numbers
这篇指南以 freeCodeCamp 开源仓库中 Basic JavaScript 递归进阶挑战(Use Recursion to Create a Range of Numbers)为蓝本,逐条拆解题目要求、6 组自动校验规则(hints)与官方参考答案,并结合同一课程块内前置的 Replace Loops Using Recursion 与 Use Recursion to Create a Countdown 挑战,帮你彻底吃透「递归自调用 + 数组构造」这一经典题型,读懂代码执行顺序与提交后如何通过免费代码编辑器内置测试。
一、挑战背景:本题目在课程中的定位
本挑战属于 basic-javascript 课程块(superblock 结构可在 basic-javascript.json 中查看),其源文件为:
curriculum/challenges/english/blocks/basic-javascript/5cc0bd7a49b71cb96132e54c.md
该 Markdown 文件是一道标准 freeCodeCamp 交互式挑战(challengeType: 1),通过 front matter 声明元数据:
| 字段 | 值 | 说明 |
|---|---|---|
id |
5cc0bd7a49b71cb96132e54c |
全局唯一挑战 ID,被数据库与工具链引用 |
title |
Use Recursion to Create a Range of Numbers | 挑战显示标题 |
challengeType |
1 |
1 表示普通代码挑战(非测验、非项目) |
forumTopicId |
301180 |
关联论坛讨论帖 ID |
dashedName |
use-recursion-to-create-a-range-of-numbers |
URL 中的短横线命名 |
从挑战描述(# --description--)原文 "Continuing from the previous challenge" 可以看出,它是「递归三部曲」的收官题目。此前的两道铺垫分别是:
- Replace Loops Using Recursion——用递归重写
for循环,掌握「基准情形(base case)+ 递归调用」的骨架,并给出multiply(arr, n) == multiply(arr, n - 1) * arr[n - 1]这一核心归约关系; - Use Recursion to Create a Countdown——返回
n到1的递减数组,验证了「递归返回前 push 使数组递增」这一反直觉现象。
本挑战在两者基础上再进一步:不再只依赖单个参数,而是通过 startNum 与 endNum 两个参数双向控制区间,用递归拼出一个递增的完整整数区间数组。
二、题目要求逐字拆解
# --instructions-- 段落给出的完整需求如下:
已定义一个名为
rangeOfNumbers的函数,含两个参数。该函数应返回一个整数数组,数组以startNum参数代表的数字开头、以endNum参数代表的数字结尾。起始数字始终小于或等于结束数字。你的函数必须通过调用自身来使用递归,且不得使用任何类型的循环。它还应当能正确处理startNum与endNum相同的情况。
把指令提炼成四条硬性约束:
| # | 约束 | 违反后果 |
|---|---|---|
| 1 | 返回 [startNum, ..., endNum] 的整数递增区间 |
断言结果不匹配 |
| 2 | 必须递归(函数内部调用自身) | toString() 源码匹配失败 |
| 3 | 禁止 for / while 及 forEach、map、filter、reduce 等高阶方法 |
正则检测直接判错 |
| 4 | startNum === endNum 时返回 [startNum] 这类单元素数组 |
单元素用例不通过 |
注意约束 3 的措辞是 "not use loops of any kind"(不使用任何循环),官方检测器虽然只对 for|while|forEach|map|filter|reduce 做正则屏蔽,但你在实现时应当把思路切换到「不迭代、只递推」,而非钻正则漏洞。
三、自动校验规则(hints)与背后的检测原理
# --hints-- 段中定义了 6 组自动测试,它们会在你每次提交代码后于浏览器端执行。看懂这些断言,等于提前看透判分标准。
3.1 返回类型必须是数组
assert(Array.isArray(rangeOfNumbers(5, 10)));
要求返回值经 Array.isArray 判定为真。若基准情形误写成 return startNum(返回裸数字),此条立刻失败。
3.2 代码中不得出现循环语法
assert(
!__helpers.removeJSComments(code).match(/for|while|forEach|map|filter|reduce/g)
);
检测器先用工具函数 removeJSComments 剥离注释,防止你用 // for 这类注释干扰正则,然后全局匹配循环关键字。这条既拦截 for/while 传统循环,也拦截 forEach、map、filter、reduce 这类高阶遍历方法——也就是说,直接用 Array.from 或 new Array(...).map() 拼接区间同样会被视为取巧思路(若包含上述关键字则直接失败)。
3.3 必须发生递归自调用
assert(
rangeOfNumbers.toString().match(/rangeOfNumbers\s*\(.+\)/)
);
这条把函数转成源码字符串再正则匹配,确认函数体内出现了以自身函数名开头的调用表达式 rangeOfNumbers(...)。它是「必须用递归」的硬性证据:只靠迭代或依赖外部工具函数都会被判不通过。
3.4 功能正确性用例(三组)
assert.deepStrictEqual(rangeOfNumbers(1, 5), [1, 2, 3, 4, 5]);
assert.deepStrictEqual(rangeOfNumbers(6, 9), [6, 7, 8, 9]);
assert.deepStrictEqual(rangeOfNumbers(4, 4), [4]);
- 普通区间:
(1, 5)与(6, 9); - 边界情形:
(4, 4)验证两参相等时能返回单元素数组[4]。
注意 deepStrictEqual 比较的是数组内容与顺序,升序排列必须严格保持。
3.5 禁止用全局变量缓存结果
rangeOfNumbers(1, 3)
assert.deepStrictEqual(rangeOfNumbers(6, 9), [6, 7, 8, 9]);
这条存在感最强也最易被忽略:先调用一次 rangeOfNumbers(1, 3) 产生副作用,随后再断言 (6, 9) 的结果。其目的(结合题面 hint 原文 "Global variables should not be used to cache the array")是防止你在模块顶层定义 let cache = [] 之类的共享可变状态——若第一次调用把 1,2,3 存进了全局数组,第二次调用继续向同一数组追加 6,7,8,9,最终返回内容就会混入历史数据而失败。递归应保证每次调用都从零开始构造一个全新的数组。
四、官方参考答案与逐步推演
4.1 解法源码
# --solutions-- 中给出的规范答案是:
function rangeOfNumbers(startNum, endNum) {
if (endNum - startNum === 0) {
return [startNum];
} else {
const numbers = rangeOfNumbers(startNum, endNum - 1);
numbers.push(endNum);
return numbers;
}
}
这道解法与 Countdown 挑战官方解法(n < 1 ? [] : [n].concat(countdown(n - 1)))在递归结构上是镜像的:Countdown 通过 [n].concat(递归结果) 实现从头拼接到尾部,本解法则采用「先让子调用返回更短的区间,再在尾部 push(endNum)」的方式。两者都严格遵守「基准情形返回值已知、不继续自调用」的原则。
4.2 基准情形分析
if (endNum - startNum === 0) {
return [startNum];
}
- 触发条件:
endNum === startNum,区间退化为单点; - 返回值:
[startNum]——已知答案,无需再递归; - 等价写法:
if (startNum === endNum) return [startNum];,官方用差值等于 0 来判定,语义上等价。
基准情形是递归的"刹车片"。回顾前置挑战 Replace Loops Using Recursion 中的警告:没有基准情形(或基准情形不可达),递归将无限执行直至调用栈溢出崩溃。
4.3 递归调用逐步推演
以 rangeOfNumbers(1, 5) 为例,演示为什么 endNum 一路递减、最终数组却递增:
| 调用层 | 参数 | 执行内容 | 返回给上一层的结果 |
|---|---|---|---|
| 第 1 层 | (1, 5) |
调 (1, 4),等待返回 |
得到 [1,2,3,4] 后 push(5) → [1,2,3,4,5] |
| 第 2 层 | (1, 4) |
调 (1, 3),等待返回 |
得到 [1,2,3] 后 push(4) → [1,2,3,4] |
| 第 3 层 | (1, 3) |
调 (1, 2),等待返回 |
得到 [1,2] 后 push(3) → [1,2,3] |
| 第 4 层 | (1, 2) |
调 (1, 1),等待返回 |
得到 [1] 后 push(2) → [1,2] |
| 第 5 层 | (1, 1) |
命中基准,返回 [1] |
—— |
关键点在于:push 发生在递归调用返回之后(对应 Countdown 挑战中强调的 "the push happens last, after the recursive call has returned")。函数先一路"向下钻"到最简情形 [startNum],再一路"向上爬"时逐层把当前 endNum 追加到子结果末尾,于是数组从小到大、从里到外被正确拼装出来。
五、变体与边界情况探讨
5.1 基于 concat 的前插写法
受 Countdown 官方解 [n].concat(countdown(n - 1)) 启发,rangeOfNumbers 也可以写成前插递归(注意递归方向变成 startNum + 1,基准情形改为 startNum > endNum):
function rangeOfNumbers(startNum, endNum) {
return startNum > endNum
? []
: [startNum].concat(rangeOfNumbers(startNum + 1, endNum));
}
- 优点:
concat直接返回新数组,无共享引用风险; - 缺点:每次递归都要复制数组,
concat在区间很大时产生更多中间数组。
官方选择 push 版本主要是为强调"递归回溯时在尾部追加"这一与 Countdown 呼应的手法。两种写法均能通过全部 hints。
5.2 为什么单元素边界必须单独处理
若省略基准情形,(4, 4) 会演变为 (4, 3)、(4, 2)……永远无法收敛,最终抛出 RangeError: Maximum call stack size exceeded。这也解释了为什么题目要求 "work for cases where both startNum and endNum are the same"——单元素区间正是递归的天然终止点。
5.3 复杂度与栈深度约束
由代码结构可以直接推断:递归深度等于区间长度 endNum - startNum + 1,数组元素个数也与之相同,因此时间与空间复杂度均为 O(n)(n 为区间长度)。受 JavaScript 引擎调用栈容量限制,递归写法不适合处理超长区间(极端输入可能栈溢出),教学场景中该函数面向的是基本 JavaScript 阶段的有限整数区间。
5.4 常见踩坑清单
| 错误写法倾向 | 失败原因 |
|---|---|
用 for 循环或 while 生成数组 |
命中 hints 3.2 正则,直接判错 |
| 把数组声明在函数体外做全局缓存 | 命中 hints 3.5 的连续调用污染检测 |
基准情形返回 [] 而不是 [startNum] |
单元素/整体结果均错误 |
| 基准情形漏写或判断条件写反 | 无限递归,运行时报栈溢出 |
在递归调用之前 push(endNum) |
顺序颠倒,产生 [5,4,3,2,1] 或重复元素 |
用 map/reduce/Array.from 偷懒 |
违反"必须递归"的题设精神并可能命中正则 |
六、该挑战文件在课程体系中的写法参考
若你是课程贡献者,本文件还是一个标准的"递归类交互式挑战"模板,值得研读其 Markdown 分区约定:
# --description--:讲解性说明(本文件此处较简短,因为知识点已在前置两题展开);# --instructions--:清晰的验收标准;# --hints--:每条 hint 是一段 JSassert表达式,直接嵌入页面侧栏作为实时测试;# --seed--下的## --seed-contents--:编辑器初始代码,仅留函数骨架:function rangeOfNumbers(startNum, endNum) { return []; };# --solutions--:挑战的官方可运行答案,用于测试套件与"查看解答"功能。
同课程块内的 Countdown 挑战(返回递减数组)与 Replace Loops Using Recursion 挑战(递归求前 n 项积/和)在编辑器中采用同一套 hints 机制,三者对比阅读,是理解 freeCodeCamp 递归教学体系最直接的素材。
七、小结
Use Recursion to Create a Range of Numbers 表面上只是"用递归造一个数组",实际考察的是三个递进能力:
- 识别基准情形:何时停止自调用并直接返回已知答案(
startNum === endNum); - 设计归约步骤:把大问题
(startNum, endNum)化为小一号问题(startNum, endNum - 1),再把当前层结果"安放"回正确位置(递归返回后push); - 规避可变共享状态:保证每次顶层调用都从全新数组开始构造。
对照官方参考答案逐层推演、跑通六组 hints,再尝试改写为 concat 前插版本验证等价性,你便同时掌握了"自顶向下分解 + 自底向上回溯"的递归思维,以及 freeCodeCamp 递归三连题的全部考点。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00