递归算法重在自然性与可理解性而非必要性;必须设明确base case防爆栈,JS因缺尾调用优化而栈深受限,树遍历等自相似结构才最适用递归。
递归算法不是“必须用递归才能解”的问题,而是“用递归更自然、更易理解”的问题。JavaScript 中能用循环解决的,绝大多数也能用递归,但不意味着该用。
没有终止条件的递归会立刻触发 RangeError: Maximum call stack size exceeded。这不是代码写错了,是 JavaScript 引擎强制保护栈空间的结果。
常见错误是把 base case 写成 n === 1 却忘了处理 n === 0 或负数输入;或者在递归调用时没让参数向 base case 靠拢(比如该减 1 却写了加 1)。
实操建议:
factorial(0) 返回 1)
null、undefined 或非数字类型导致隐式转换出错V8 引擎(Chrome / Node.js)默认栈深度约 10000–15000 层,但实际能安全使用的远低于此——尤其在嵌套对象深拷贝、DOM 树遍历等场景下,几百层就可能出问题。
这不是算法本身的问题,是 JS 缺乏尾调用优化(TCO)的实际限制:ES2015 规范虽定义了 TCO,但 V8 目前仅在 strict mode + 纯尾调用形式下极有限支持,且已被标记为“暂不优先实现”。
实操建议:
stack 数组模拟调用栈(如 DFS)setTimeout 或 queueMicrotask 拆解任务)扁平化嵌套菜单、计算 AST 节点数量、序列化带循环引用的对象(需缓存已访问对象)——这些操作天然符合“自相似子结构”的特征,递归比手动维护栈更直观。
示例:简单二叉树节点计数
function countNodes(node) {
if (!node) return 0;
return 1 + countNodes(node.left) + countNodes(node.right);
}
注意这里没有副作用、无状态累积,也不依赖外部变量——这是可读性高的递归的关键特征。一旦混入全局变量或多次修改同一对象,调试难度会指数上升。
实操建议:
WeakMap 记录已访问引用,防止无限循环for 或 reduce 更稳妥真正难的不是写出能跑的递归,是判断什么时候不该用它——尤其是当调用链可能由用户输入长度决定时,栈溢出往往发生在上线后某个边缘请求里。