- 文档
- 教程
- 前端
【免费下载链接】Web
千古前端图文教程,超详细的前端入门到进阶知识库。从零开始学前端,做一名精致优雅的前端工程师。
导读
递归(Recursion)是 JavaScript 乃至整个数据结构和算法领域的核心思维:如果一个函数在内部调用这个函数自身,这个函数就是递归函数。递归能够把很多复杂的数据模型拆解为简单问题进行求解,是每一位前端工程师必须掌握的基本功。本篇教程以《04-JavaScript基础/21-递归函数.md》为主线,完整讲解递归的概念、两大要素(递归体与递归出口),并配套阶乘、喇叭花数、斐波那契数列三大实战案例,最后结合仓库中深拷贝实现、arguments.callee 等源码级佐证,讲透递归在前端真实项目中的落地方式。读完本篇,你将能独立写出正确、安全、可读的递归函数,并理解递归与循环、深拷贝之间的联系。
一、什么是递归函数
概念
递归函数是指在函数体内部调用函数自身的函数。用一句最简单的话概括:
如果一个函数在内部调用这个函数自身,这个函数就是递归函数。
递归在数据结构和算法中经常用到,可以将很多复杂的数据模型拆解为简单问题进行求解,因此一定要掌握。典型应用场景包括:树的遍历(DOM 树、组件树)、深拷贝、目录遍历、排序算法(快排、归并)、斐波那契数列、阶乘计算等。
一个最简单的递归示例:
function countDown(n) { if (n === 0) { console.log('发射!'); return; // 递归出口:不再调用自身 } console.log(n); countDown(n - 1); // 递归调用:调用函数自身 } countDown(5);递归的两大要素
要写出一个正确的递归函数,必须同时具备两个要素:
- 递归模式(递归体):把大问题拆解为小问题进行分析。即:找到原问题与规模更小的子问题之间的递推关系,在函数体内通过调用自身来完成"分解——求解——合并"的过程。
- 边界条件(递归出口):确定递归到何时结束。即:当问题规模小到可以直接求解时,不再调用自身,直接返回结果。缺少边界条件的递归会无限调用自身,最终导致调用栈溢出(Stack Overflow)。
可以把递归理解成"俄罗斯套娃":每一层套娃都在重复同样的动作,但规模逐层变小,直到最小的那一层可以直接处理(边界条件),然后逐层返回结果。
二、代码演示:计算阶乘
提问:求一个正整数的阶乘。
阶乘的定义:n! = 1 × 2 × 3 × ... × n,例如5! = 5 × 4 × 3 × 2 × 1 = 120。
普通写法(循环)
在没有学习递归之前,我们通常用循环累乘来实现:
// 函数:计算一个正整数的阶乘 function factorial(n) { let result = 1; for (let i = 1; i <= n; i++) { result *= i; } return result; } console.log(factorial(5)); // 120递归写法
学习了递归之后,阶乘可以写出更简洁、更贴合数学定义的写法:
// 递归函数:计算一个正整数的阶乘 function factorial(n) { // 递归出口:如果计算1的阶乘,就不用递归了 if (n == 1) return 1; // 开始递归:如果当前这个 n 不是1,就返回 n * (n-1)! return n * factorial(n - 1); } console.log(factorial(5)); // 120执行过程拆解:factorial(5)需要5 * factorial(4),factorial(4)需要4 * factorial(3)……直到factorial(1)直接返回 1,然后逐层回溯相乘:1 * 2 = 2,2 * 3 = 6,6 * 4 = 24,24 * 5 = 120。整个过程与阶乘的数学递推式n! = n × (n - 1)!完全对应,可见递归写法的本质是用代码直接表达数学递推关系。
对比两种写法:循环写法依赖"计数器 + 累乘变量",递归写法依赖"函数自身调用 + 出口判断"。两者都能正确求解,但递归写法更贴近问题本身的数学结构,可读性更强;代价是多了一次次的函数调用开销。对于阶乘这类单层线性递推,两者效率相当;但在树形、分治类问题(如深拷贝、目录遍历)中,递归往往是唯一自然的解法。
三、案例一:寻找所有的喇叭花数
题目:喇叭花数是一个三位数,其每一位数字的阶乘之和恰好等于它本身,即abc = a! + b! + c!,其中abc表示一个三位数。请找出所有的喇叭花数。
思路:将计算某个数字的阶乘封装成递归函数factorial,然后穷举遍历 100~999 的所有三位数,逐位取出百位、十位、个位,代入条件判断。
代码实现:
// 递归函数:计算一个数的阶乘 function factorial(n) { // 递归出口:如果计算1的阶乘,就不用递归了 if (n == 1) return 1; // 开始递归:如果当前这个 n 不是1,就返回 n * (n-1)! return n * factorial(n - 1); } // 穷举法,从100到999遍历,寻找喇叭花数 for (let i = 100; i <= 999; i++) { // 将数字i转为字符串 const i_str = i.toString(); // abc分别表示百位、十位、个位 const a = Number(i_str[0]); const b = Number(i_str[1]); const c = Number(i_str[2]); // 根据喇叭花数的条件进行判断 if (factorial(a) + factorial(b) + factorial(c) == i) { console.log(i); } }打印结果:
145验证:1! + 4! + 5! = 1 + 24 + 120 = 145,正好等于它本身,因此 145 是唯一的喇叭花数。
这个案例展示了递归的典型配合方式:递归函数负责求解子问题(某一位数字的阶乘),外层循环负责穷举与组合。这也是递归最常见的工程形态——递归不孤立存在,而是与遍历、穷举、分治等策略协同工作。
四、案例二:斐波那契数列
斐波那契数列是这样一个数列:1、1、2、3、5、8、13、21、34……,最早是由意大利数学家斐波那契开始研究的。它的规律是:下标为 0 和 1 的项,值为 1;从下标为 2 的项开始,每一项等于前面两项之和。
提问:请找出斐波那契数列的前 10 项。
代码实现:
// 递归函数:返回斐波那契数列中下标为n的那一项的值 function fib(n) { // 下标为0和1的项,值为1 if (n == 0 || n == 1) return 1; // 从下标为2的项开始,每一项等于前面两项之和 return fib(n - 1) + fib(n - 2); } // 循环语句:打印斐波那契数列的前10项 for (let i = 0; i < 10; i++) { console.log(fib(i)); }打印结果:
1 1 2 3 5 8 13 21 34 55(原文档代码中循环取i < 15打印了前 15 项,若要严格打印前 10 项可改用i < 10,两种写法输出序列一致、均可运行。)
递归关系分析:fib(n)把问题拆解为两个子问题fib(n - 1)和fib(n - 2),递归出口是n == 0 || n == 1时直接返回 1。这里的递归体是一条分叉递推式(一次调用会派生出两次子调用),比阶乘的单链递归复杂,调用路径呈树状展开。
知识延伸:递归的性能局限。以fib为例,fib(n)会重复计算大量相同的子问题(例如fib(4)会被fib(6)和fib(5)分别计算),导致指数级的时间开销。工程实践中常配合记忆化(Memoization)——用一个缓存对象保存已算过的fib(n)结果,或直接用循环迭代、动态规划来改写。这是从"学会递归"走向"用好递归"的关键一步。
五、小结:递归的更多应用场景
关于递归的案例,今后我们还会学习更多的应用场景。比如深拷贝就会用到递归。
这一点在仓库的源码文档中有直接印证:
- 在 04-JavaScript基础/30-浅拷贝和深拷贝.md 中明确指出"深拷贝其实就是将浅拷贝进行递归",并给出了
for in递归实现:遍历对象的每个属性,若属性值是数组则newObj[key] = []并递归拷贝,若是对象则newObj[key] = {}并递归拷贝,若是基本类型则直接赋值:
// 方法:深拷贝 function deepCopy(newObj, oldObj) { for (let key in oldObj) { // 获取属性值 oldObj[key] let item = oldObj[key]; // 判断这个值是否是数组 if (item instanceof Array) { newObj[key] = []; deepCopy(newObj[key], item); } else if (item instanceof Object) { // 判断这个值是否是对象 newObj[key] = {}; deepCopy(newObj[key], item); } else { // 简单数据类型,直接赋值 newObj[key] = item; } } }- 在 07-JavaScript进阶/02-浅拷贝和深拷贝.md 中,还提供了另一种手写递归的
deepClone写法,先判断值类型还是引用类型(typeof obj !== 'object' || obj == null时直接返回,这就是递归出口),再判断数组或对象,随后用result[key] = deepClone(obj[key])递归拷贝每一个属性。
深拷贝的递归与阶乘、斐波那契如出一辙:每遇到一个嵌套的复合类型,就调用自身把"拷贝"动作应用到下一层,直到所有值都变成可直接赋值的基本类型——这正是"递归模式 + 边界条件"两大要素的又一次实战体现。
六、仓库源码佐证:递归在函数与 Node 生态中的更多落点
arguments.callee 与递归
在 04-JavaScript基础/20-函数简介.md 中,仓库讲解了arguments.callee的用法:arguments里边的callee属性对应当前正在执行的函数对象。文档特别指出:
在使用函数递归调用时,推荐使用 arguments.callee 代替函数名本身。
示例:
function fun() { console.log(arguments.callee == fun); // 打印结果为true } fun('hello');也就是说,当函数需要递归调用自身、但又希望函数体与函数名解耦时,可以使用arguments.callee引用当前函数。不过需要说明的是,在 ES5 严格模式下arguments.callee已被禁止,现代开发中更推荐使用具名函数表达式或直接调用函数名。
递归参数在 Node.js 内置模块中的体现
递归思想不仅存在于算法层面,也直接渗透到 Node.js 的 API 设计中。在 11-Node.js/05-Node.js内置模块:fs文件模块.md 中,fs.mkdir的options参数就包含:
recursive:是否以递归的方式创建目录,默认为false;mode:设置目录权限,默认为0777。
当recursive: true时,Node.js 底层会递归创建路径中的每一级目录——这正是"递归把复杂问题(创建多层目录)拆解为简单问题(逐级创建单层目录)"的又一个真实工程案例。
七、总结
| 要点 | 内容 |
|---|---|
| 递归概念 | 函数体内部调用函数自身 |
| 递归模式(递归体) | 把大问题拆解为小问题,找到递推关系 |
| 边界条件(递归出口) | 确定递归何时结束,防止无限递归/栈溢出 |
| 经典案例 | 阶乘(单链递归)、斐波那契(分叉递归)、喇叭花数(递归 + 穷举) |
| 工程落地 | 深拷贝(递归遍历嵌套对象/数组)、目录递归创建、树形数据遍历 |
| 性能提醒 | 注意重复子问题,可配合记忆化或循环迭代优化 |
掌握了"递归模式 + 边界条件"两要素,再配合本仓库中深拷贝、浅拷贝与深拷贝进阶、函数与 arguments 专题 等配套文档继续深入,你就能把递归这一核心武器熟练运用于前端开发的方方面面。
- 文档
- 教程
- 前端
【免费下载链接】Web
千古前端图文教程,超详细的前端入门到进阶知识库。从零开始学前端,做一名精致优雅的前端工程师。
相关推荐
Python递归算法优化:gh_mirrors/da/data-science-interviews项目阶乘与斐波那契尾递归实现
Python递归算法优化:gh_mirrors/da/data science interviews项目阶乘与斐波那契尾递归实现 递归是Python编程中解决复
文档知识库数据科学教程用 JavaScript 递归实战:斐波那契数列与归并排序(Fibonacci & Merge Sort)
用 JavaScript 递归实战:斐波那契数列与归并排序(Fibonacci & Merge Sort) 导读 本篇实战项目来自 curriculum htt
文档教程教育终极算法指南:Algorithms项目中的递归与迭代实战对比——从斐波那契数列到阶乘计算
终极算法指南:Algorithms项目中的递归与迭代实战对比——从斐波那契数列到阶乘计算 Algorithms项目是一个专注于用Java解决常见算法问题的开源项
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考