首页
/ freeCodeCamp 基础 JavaScript 实战:用递归(Recursion)替换循环,彻底掌握 base case 与分治思维

freeCodeCamp 基础 JavaScript 实战:用递归(Recursion)替换循环,彻底掌握 base case 与分治思维

2026-09-07 13:12:04作者:何将鹤

本篇技术指南以 freeCodeCamp 开源仓库中 Basic JavaScript 板块的经典挑战「Replace Loops using Recursion」为线索,讲解如何将一个用 for 循环实现的数组求积函数改写为纯递归版本,并最终独立完成数组求和递归函数 sum(arr, n)。读完你将掌握递归的两大基石(递归基 case 与递归步)、如何识别“循环可改写为递归”的数学恒等式,以及 freeCodeCamp 如何通过自动化断言来约束“禁用循环、必须递归”的代码风格。

挑战背景:在 Basic JavaScript 板块中的位置

「Replace Loops using Recursion」是 freeCodeCamp 官方开源课程中 Basic JavaScript 挑战块 的一个核心关卡,其挑战 ID 为 5cfa3679138e7d9595b9d9d4dashedName(URL 友好标识)为 replace-loops-using-recursion

basic-javascript.json 的结构定义(该 ID 出现在第 415 行附近的挑战顺序数组中)可以看出,这道题紧跟在「Iterate with JavaScript Do...While Loops」「Nesting For Loops」等循环专题之后,紧接着下一题是「Profile Lookup」。这意味着它是循环章节的收尾关卡:freeCodeCamp 在教完 forwhiledo...while 以及数组遍历之后,特意安排一道要求禁用循环、改用递归解决的题目,帮助学习者完成从“迭代思维”到“递归思维”的切换。

在挑战前置元数据中,challengeType: 1 对应仓库中共享配置 packages/shared/src/config/challenge-types.ts 所定义的 js 类型(const js = 1;),即这是一道在浏览器内直接编辑、运行并接受测试断言的标准 JavaScript 编程题。挑战正文同时经过 curriculum/schema/challenge-schema.js 中 Joi 模式的字段校验,保证每个挑战文件都能被课程生成系统正确解析。

核心概念:当循环可以用“自己定义自己”来表达

挑战的描述部分给出了全文最关键的洞察。假设要计算数组 arrn 个元素的乘积,教科书式的 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 casen <= 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 注释剥离(防止把关键字写在注释里蒙混过关),再检查是否出现 forwhileforEachmapfilterreduce 中任意一个词。只要命中其一,测试即失败。这意味着:连高阶函数风格的隐式迭代(如 reduce)都被禁止,唯一合法路径就是递归。

断言三:确认代码中确实发生了递归调用

assert(
  sum.toString().length > 1 &&
  sum.toString().match(/sum\(.*\)/g).length > 1
);

这条断言通过把函数对象序列化为源码字符串并统计 sum(...) 的调用次数,确认 sum 内部至少有一次对自身的调用——即函数体内确实存在递归调用点,而非只写了 return 语句敷衍了事。

以上三条断言层层互补:断言一管“算得对”,断言二管“不许迭代”,断言三管“必须递归”,共同把解题过程收敛到唯一正确的思维路径上。

从这道题延伸:何时该用递归替换循环?

本挑战表面上只是一道入门练手题,但它背后的判断力对所有 JavaScript 开发者都至关重要。综合挑战描述与解法,可以提炼出几条实用的经验准则:

  1. 识别自相似结构:当问题可以被拆解为“规模更小的同构子问题 + 一步简单运算”(如乘一个元素、加一个元素),就存在递归改写的可能。典型地,multiply(arr, n)sum(arr, n) 都属于“对数组前缀做二元折叠”的结构。
  2. 先找 base case,再写递归步:编写递归的第一件事永远是明确“什么情况下不需要继续调用自己”,然后找到对应的恒等元(乘法为 1、加法为 0)。base case 缺失或写错是无限递归的头号原因。
  3. 递归优雅但不总是最优:每次递归调用都会在调用栈上占用一帧,对超长数组或极深嵌套可能触发栈溢出。对于本题这类线性前缀计算,迭代通常更省空间;递归的价值更多在于思维上的简洁性,以及它天然适配树、图、分治等本身就具有递归结构的算法场景。
  4. 理解函数的“参数即状态”:递归版的 sum(arr, n - 1) 之所以可行,是因为 n 作为不可变参数在每次调用中递减,从而替代了循环变量 i 的角色。这正是“用递归替换循环”背后最本质的转换技巧。

如果你想继续在 freeCodeCamp 的课程体系中深化这一能力,可在 Basic JavaScript 板块后续挑战中观察「Profile Lookup」等综合题目如何组合运用数组、对象与遍历逻辑,也可前往本仓库的 curriculum/challenges/english/blocks/basic-javascript/ 目录,浏览上百道采用同样 Markdown 元数据格式编写的相邻挑战,横向比较各题的描述、hints 与解决方案写法,进一步理解 freeCodeCamp 自动化测评体系的设计思路。

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

项目优选

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