首页
/ freeCodeCamp Basic JavaScript 挑战解析:Use Recursion to Create a Range of Numbers

freeCodeCamp Basic JavaScript 挑战解析:Use Recursion to Create a Range of Numbers

2026-09-07 16:18:26作者:贡沫苏Truman

这篇指南以 freeCodeCamp 开源仓库中 Basic JavaScript 递归进阶挑战(Use Recursion to Create a Range of Numbers)为蓝本,逐条拆解题目要求、6 组自动校验规则(hints)与官方参考答案,并结合同一课程块内前置的 Replace Loops Using RecursionUse 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" 可以看出,它是「递归三部曲」的收官题目。此前的两道铺垫分别是:

  1. Replace Loops Using Recursion——用递归重写 for 循环,掌握「基准情形(base case)+ 递归调用」的骨架,并给出 multiply(arr, n) == multiply(arr, n - 1) * arr[n - 1] 这一核心归约关系;
  2. Use Recursion to Create a Countdown——返回 n1 的递减数组,验证了「递归返回前 push 使数组递增」这一反直觉现象。

本挑战在两者基础上再进一步:不再只依赖单个参数,而是通过 startNumendNum 两个参数双向控制区间,用递归拼出一个递增的完整整数区间数组

二、题目要求逐字拆解

# --instructions-- 段落给出的完整需求如下:

已定义一个名为 rangeOfNumbers 的函数,含两个参数。该函数应返回一个整数数组,数组以 startNum 参数代表的数字开头、以 endNum 参数代表的数字结尾。起始数字始终小于或等于结束数字。你的函数必须通过调用自身来使用递归,且不得使用任何类型的循环。它还应当能正确处理 startNumendNum 相同的情况。

把指令提炼成四条硬性约束:

# 约束 违反后果
1 返回 [startNum, ..., endNum] 的整数递增区间 断言结果不匹配
2 必须递归(函数内部调用自身) toString() 源码匹配失败
3 禁止 for / whileforEachmapfilterreduce 等高阶方法 正则检测直接判错
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 传统循环,也拦截 forEachmapfilterreduce 这类高阶遍历方法——也就是说,直接用 Array.fromnew 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 是一段 JS assert 表达式,直接嵌入页面侧栏作为实时测试;
  • # --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 表面上只是"用递归造一个数组",实际考察的是三个递进能力:

  1. 识别基准情形:何时停止自调用并直接返回已知答案(startNum === endNum);
  2. 设计归约步骤:把大问题 (startNum, endNum) 化为小一号问题 (startNum, endNum - 1),再把当前层结果"安放"回正确位置(递归返回后 push);
  3. 规避可变共享状态:保证每次顶层调用都从全新数组开始构造。

对照官方参考答案逐层推演、跑通六组 hints,再尝试改写为 concat 前插版本验证等价性,你便同时掌握了"自顶向下分解 + 自底向上回溯"的递归思维,以及 freeCodeCamp 递归三连题的全部考点。

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

项目优选

收起
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