递归函数是解决天然具有自相似结构问题最直接的方式,需满足两个条件:存在基础情况(base case)且每次递归必须逼近该情况,否则将爆栈。
递归函数不是“必须用循环替代的炫技写法”,而是解决**天然具有自相似结构的问题**最直接的方式——比如遍历树、解析嵌套对象、计算阶乘、实现快速排序等。关键不在“能不能写”,而在“要不要写”和“会不会爆栈”。
所有安全可用的递归函数都必须同时满足:
base case(基础情况),即无需再次调用自身就能直接返回结果的条件;base case 靠近,否则会无限调用直至触发 RangeError: Maximum call stack size exceeded。以计算阶乘为例:n! = n

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,但实际能跑多深取决于环境和调用链长度。
递归在 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 嵌套清晰得多。