递归函数需满足三要素:明确终止条件、每次调用逼近终止、返回值参与当前计算;适用于天然递归结构如树遍历、嵌套数据处理,而非简单循环替代。

递归函数是用自身来调用自身的一种方式,适合解决具有重复子结构的问题,比如阶乘、斐波那契数列、树遍历、嵌套数组扁平化等。它不是“替代循环”的万能写法,而是把问题不断拆解为更小的同类问题,直到达到一个明确的终止条件(即递归出口)。
递归的核心三要素
写一个安全有效的递归函数,必须同时满足以下三点:
-
有明确的终止条件(base case):防止无限调用导致栈溢出。例如计算阶乘时,
n === 0或n === 1就直接返回1。 -
每次递归都向终止条件靠近:比如传入的参数要变小(
n - 1)、变短(arr.slice(1))或变浅(处理子节点而非当前节点)。 -
递归调用的结果要参与当前层的计算:不能只调用却不使用返回值,否则逻辑就断了。例如
return n * factorial(n - 1)中,factorial(n - 1)的结果被用来乘n。
用递归代替 for/while 的典型例子
比如求一个整数的阶乘(n! = n × (n−1) × … × 1):
function factorial(n) {
if (n < 0) return NaN; // 边界检查
if (n === 0 || n === 1) return 1; // 终止条件
return n * factorial(n - 1); // 自调用,并参与计算
}
再比如扁平化一个深度未知的嵌套数组:
立即学习“Java免费学习笔记(深入)”;
function flatten(arr) {
let result = [];
for (let item of arr) {
if (Array.isArray(item)) {
result = result.concat(flatten(item)); // 对子数组递归处理
} else {
result.push(item);
}
}
return result;
}
这个版本虽用了 for,但核心逻辑靠递归推进;也可以完全去掉循环,改用展开运算符 + 递归判断:
function flatten(arr) {
return arr.reduce((acc, item) => {
return acc.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
递归 vs 循环:什么时候选递归?
递归更适合描述“结构天然递归”的场景,比如:
- DOM 树或虚拟 DOM 的深度遍历
- 文件系统中查找某类后缀的文件(含子目录)
- 解析 JSON 或嵌套配置对象
- 回溯算法(如八皇后、全排列)
而简单计数、累加、遍历已知长度数组等,用 for 或 reduce 更直观、性能更好、不易爆栈。
避免常见坑:栈溢出与尾递归优化
JavaScript 引擎对递归深度有限制(一般几千层),深层嵌套易触发 RangeError: Maximum call stack size exceeded。可考虑:
- 加最大深度限制并提前报错
- 用“迭代+栈模拟”重写为非递归版本(显式维护调用栈)
- 在支持的环境(如 Safari)中尝试尾递归优化(需严格满足尾调用形式,且函数最后一句是纯递归调用)
例如尾递归版阶乘(需启用严格模式,且仅部分引擎支持优化):
"use strict";
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc); // 尾位置调用,无后续计算
}


















