freeCodeCamp 基础 JavaScript 实战:用递归(Recursion)替换循环,彻底掌握 base case 与分治思维
本篇技术指南以 freeCodeCamp 开源仓库中 Basic JavaScript 板块的经典挑战「Replace Loops using Recursion」为线索,讲解如何将一个用 for 循环实现的数组求积函数改写为纯递归版本,并最终独立完成数组求和递归函数 sum(arr, n)。读完你将掌握递归的两大基石(递归基 case 与递归步)、如何识别“循环可改写为递归”的数学恒等式,以及 freeCodeCamp 如何通过自动化断言来约束“禁用循环、必须递归”的代码风格。
挑战背景:在 Basic JavaScript 板块中的位置
「Replace Loops using Recursion」是 freeCodeCamp 官方开源课程中 Basic JavaScript 挑战块 的一个核心关卡,其挑战 ID 为 5cfa3679138e7d9595b9d9d4,dashedName(URL 友好标识)为 replace-loops-using-recursion。
从 basic-javascript.json 的结构定义(该 ID 出现在第 415 行附近的挑战顺序数组中)可以看出,这道题紧跟在「Iterate with JavaScript Do...While Loops」「Nesting For Loops」等循环专题之后,紧接着下一题是「Profile Lookup」。这意味着它是循环章节的收尾关卡:freeCodeCamp 在教完 for、while、do...while 以及数组遍历之后,特意安排一道要求禁用循环、改用递归解决的题目,帮助学习者完成从“迭代思维”到“递归思维”的切换。
在挑战前置元数据中,challengeType: 1 对应仓库中共享配置 packages/shared/src/config/challenge-types.ts 所定义的 js 类型(const js = 1;),即这是一道在浏览器内直接编辑、运行并接受测试断言的标准 JavaScript 编程题。挑战正文同时经过 curriculum/schema/challenge-schema.js 中 Joi 模式的字段校验,保证每个挑战文件都能被课程生成系统正确解析。
核心概念:当循环可以用“自己定义自己”来表达
挑战的描述部分给出了全文最关键的洞察。假设要计算数组 arr 前 n 个元素的乘积,教科书式的 for 循环写法如下:
function multiply(arr, n) {
let product = 1;
for (let i = 0; i < n; i++) {
product *= arr[i];
}
return product;
}
循环的思路是:维护一个累乘变量 product,依次把 arr[0]、arr[1]……乘进去。而挑战点破了一个看似平凡却极其重要的恒等式:
multiply(arr, n) == multiply(arr, n - 1) * arr[n - 1]
也就是说,“前 n 个元素之积”可以拆解为“前 n - 1 个元素之积,再乘以第 n 个元素(下标 n - 1)”。这个大问题不断地收缩为规模减一的同构子问题,最终必然收敛到一个可以立即回答的边界。既然函数可以“用自身来表达自身”,循环就不再是必需品。
基于该恒等式的递归写法如下:
function multiply(arr, n) {
if (n <= 0) {
return 1;
} else {
return multiply(arr, n - 1) * arr[n - 1];
}
}
注意这里的空积(empty product)约定:当 n <= 0 时没有任何元素可乘,乘积恒等元为 1,这正是乘法场景下的递归基。挑战描述中也以 <dfn> 语义高亮了关键术语 base case(递归基/基线条件)。
递归的执行拆解:调用栈如何一步步折叠
挑战描述指出,上述递归版 multiply 的执行流程如下:在 base case(n <= 0)下直接返回 1;对更大的 n,函数以 n - 1 调用自身,该调用又按同样的规则继续调用 multiply,直到参数降到 n <= 0;此时所有挂起的调用依次返回,最初的 multiply 最终得到答案。
仍以 multiply([2, 3, 4], 3) 为例,调用链会是这样展开的:
multiply([2,3,4], 3)
= multiply([2,3,4], 2) * 4
= (multiply([2,3,4], 1) * 3) * 4
= ((multiply([2,3,4], 0) * 2) * 3) * 4
= ((1 * 2) * 3) * 4
= 24
每一层递归都先把问题“悬置”起来,等待更深层的调用返回结果后再做一次乘法。这种“先递后归、层层回卷”的行为正是调用栈(call stack)的运作方式——每一帧(frame)保存了当前层的局部状态与返回地址。
挑战中反复强调的警告
挑战在描述中特别用 Note 标注了一个初学者最容易踩的坑:
注意: 递归函数必须拥有一个不再继续调用自身的 base case(本例中即
n <= 0时直接返回),否则它将永远无法结束执行。
这正是无限递归(导致 RangeError: Maximum call stack size exceeded)的根源。缺少基线条件,递归层数会无限增长,最终耗尽调用栈。
实战任务:编写 sum(arr, n) 递归函数
在充分理解 multiply 的改写逻辑后,挑战的 --instructions-- 给出了你的独立任务:
编写一个递归函数
sum(arr, n),返回数组arr中前n个元素之和。
这道题与示例中的乘积问题在结构上完全同构,只是把乘法运算换成加法。相应的恒等式为:
sum(arr, n) == sum(arr, n - 1) + arr[n - 1]
空和(empty sum)的恒等元是 0,因此递归基在 n <= 0 时应返回 0。
初始代码(seed)
挑战会在编辑器中预置如下模板,你只需在 // Only change code below this line 与 // Only change code above this line 两个注释标记之间补全逻辑:
function sum(arr, n) {
// Only change code below this line
// Only change code above this line
}
参考解法(solutions)
挑战元数据中内置的参考实现如下,它忠实遵循“递归基 + 递归步”的范式:
function sum(arr, n) {
// Only change code below this line
if(n <= 0) {
return 0;
} else {
return sum(arr, n - 1) + arr[n - 1];
}
// Only change code above this line
}
对照验证一个用例:sum([2, 3, 4, 5], 3) 表示只取数组前 3 个元素 [2, 3, 4] 求和,递归过程为:
sum([2,3,4,5], 3)
= sum([2,3,4,5], 2) + 4
= (sum([2,3,4,5], 1) + 3) + 4
= ((sum([2,3,4,5], 0) + 2) + 3) + 4
= ((0 + 2) + 3) + 4
= 9
最终答案为 9,与挑战 hints 中的断言一致。
一个容易混淆的坑:n 是“数量”而非“下标”
许多初学者会把 arr[n] 误写成参与计算。请务必牢记:n 表示取前几个元素,最后一个参与计算的元素下标是 n - 1。例如 sum(arr, 3) 计算的是 arr[0] + arr[1] + arr[2],绝不包含 arr[3]。这一语义在 multiply 示例(multiply(arr, n - 1) * arr[n - 1])中已经埋下伏笔,做题时请对照恒等式逐字检查。
自动化测试与“禁用循环”约束是如何强制执行的
每道 freeCodeCamp 挑战都附带 --hints-- 段,由测试断言驱动你写出正确的实现。本题的测试设计尤其值得研究,因为它不仅验证结果正确性,还严格限制了解题手段。我们可以从本挑战文件(5cfa3679138e7d9595b9d9d4.md)的源文件中看到完整断言。
断言一:结果正确性(覆盖 base case 与递归步)
assert.equal(sum([1], 0), 0);
assert.equal(sum([2, 3, 4], 1), 2);
assert.equal(sum([2, 3, 4, 5], 3), 9);
三个用例分别验证了不同规模的输入:
| 调用 | 实际含义 | 期望值 | 验证点 |
|---|---|---|---|
sum([1], 0) |
前 0 个元素之和 | 0 |
触发 base case(n <= 0 返回 0) |
sum([2, 3, 4], 1) |
前 1 个元素之和 | 2 |
仅执行一层递归即触底 |
sum([2, 3, 4, 5], 3) |
前 3 个元素之和 | 9 |
多层递归叠加求和的完整路径 |
断言二:静态检查禁用所有形式的循环
真正体现这道题“不许偷懒”的是这条代码静态检查断言:
assert(
!__helpers.removeJSComments(code).match(/for|while|forEach|map|filter|reduce/g)
);
它先将用户代码中的 JS 注释剥离(防止把关键字写在注释里蒙混过关),再检查是否出现 for、while、forEach、map、filter、reduce 中任意一个词。只要命中其一,测试即失败。这意味着:连高阶函数风格的隐式迭代(如 reduce)都被禁止,唯一合法路径就是递归。
断言三:确认代码中确实发生了递归调用
assert(
sum.toString().length > 1 &&
sum.toString().match(/sum\(.*\)/g).length > 1
);
这条断言通过把函数对象序列化为源码字符串并统计 sum(...) 的调用次数,确认 sum 内部至少有一次对自身的调用——即函数体内确实存在递归调用点,而非只写了 return 语句敷衍了事。
以上三条断言层层互补:断言一管“算得对”,断言二管“不许迭代”,断言三管“必须递归”,共同把解题过程收敛到唯一正确的思维路径上。
从这道题延伸:何时该用递归替换循环?
本挑战表面上只是一道入门练手题,但它背后的判断力对所有 JavaScript 开发者都至关重要。综合挑战描述与解法,可以提炼出几条实用的经验准则:
- 识别自相似结构:当问题可以被拆解为“规模更小的同构子问题 + 一步简单运算”(如乘一个元素、加一个元素),就存在递归改写的可能。典型地,
multiply(arr, n)与sum(arr, n)都属于“对数组前缀做二元折叠”的结构。 - 先找 base case,再写递归步:编写递归的第一件事永远是明确“什么情况下不需要继续调用自己”,然后找到对应的恒等元(乘法为
1、加法为0)。base case 缺失或写错是无限递归的头号原因。 - 递归优雅但不总是最优:每次递归调用都会在调用栈上占用一帧,对超长数组或极深嵌套可能触发栈溢出。对于本题这类线性前缀计算,迭代通常更省空间;递归的价值更多在于思维上的简洁性,以及它天然适配树、图、分治等本身就具有递归结构的算法场景。
- 理解函数的“参数即状态”:递归版的
sum(arr, n - 1)之所以可行,是因为n作为不可变参数在每次调用中递减,从而替代了循环变量i的角色。这正是“用递归替换循环”背后最本质的转换技巧。
如果你想继续在 freeCodeCamp 的课程体系中深化这一能力,可在 Basic JavaScript 板块后续挑战中观察「Profile Lookup」等综合题目如何组合运用数组、对象与遍历逻辑,也可前往本仓库的 curriculum/challenges/english/blocks/basic-javascript/ 目录,浏览上百道采用同样 Markdown 元数据格式编写的相邻挑战,横向比较各题的描述、hints 与解决方案写法,进一步理解 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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
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