递归函数是解决天然具有自相似结构问题最直接的方式,需满足两个条件:存在基础情况(base case)且每次递归必须逼近该情况,否则将爆栈。

递归函数不是“必须用循环替代的炫技写法”,而是解决**天然具有自相似结构的问题**最直接的方式——比如遍历树、解析嵌套对象、计算阶乘、实现快速排序等。关键不在“能不能写”,而在“要不要写”和“会不会爆栈”。
递归函数的两个必要条件
所有安全可用的递归函数都必须同时满足:
- 有一个或多个
base case(基础情况),即无需再次调用自身就能直接返回结果的条件; - 每次递归调用都必须向
base case靠近,否则会无限调用直至触发RangeError: Maximum call stack size exceeded。
JavaScript 中写递归函数的典型结构
以计算阶乘为例:n! = n × (n−1)!,而 0! = 1 是自然终止点。
function factorial(n) {
if (n < 0) return NaN; // 非法输入防护
if (n === 0 || n === 1) return 1; // base case
return n * factorial(n - 1); // 递归调用,n 严格减小
}注意:factorial(5) 会生成 5 层调用栈,factorial(10000) 很可能崩溃——V8 引擎默认栈深度约 10000~15000,但实际能跑多深取决于环境和调用链长度。
立即学习“Java免费学习笔记(深入)”;
常见误用与坑点
递归在 JS 中容易被写成“伪递归”或“危险递归”,尤其在处理异步、对象引用、数组索引时:
- 忘记检查
base case或写错条件(如用n 却没处理负数,导致factorial(-1)死循环); - 递归调用未改变参数(如写成
factorial(n)而非factorial(n - 1)); - 对深层嵌套对象递归遍历时,没检测循环引用,引发无限展开(
JSON.stringify就因此抛错); - 在事件处理或定时器中无节制递归(如
setTimeout(fn, 0)+ 立即调用fn()),表面像递归实则失控。
什么时候该避免递归?
当问题可以线性迭代完成,且没有明显分治/嵌套结构时,优先用 for 或 while。例如遍历数组求和、字符串翻转、简单累加——这些用递归不仅慢,还占栈空间,也没有可读性优势。
真正值得递归的场景,是代码逻辑和问题结构完全对齐:比如解析一个 menu 对象,它有 children 数组,每个子项又可能有 children……这时候一个 renderMenu(item) 函数调自己一次,比写三层 for 嵌套清晰得多。











