← 中文资源

简体中文版本

JavaScript 调用栈溢出

定位无界递归或循环遍历导致的 RangeError,通过迭代、基线条件或 visited 集合打破循环。

阅读英文原文

症状

出现 RangeError: Maximum call stack size exceeded;渲染或解析深层嵌套结构时标签页冻结或崩溃;只在对深嵌套或自引用数据时出现。

诊断

在 DevTools Console 中展开 RangeError 堆栈;同一个函数重复出现通常指向递归,记录触发输入。用最小嵌套或循环输入复现:

node -e "const a = {}; a.self = a; JSON.stringify(a)"

如果抛出循环结构的 TypeError,可确认存在环;RangeError 则确认递归深度。临时加入深度计数器或 visited 集合,观察输入是否比预期更深或存在循环。

根因

每个函数调用消耗一个栈帧。递归深度或相互递归超过引擎限制时,JavaScript 抛出 RangeError。循环数据或缺失终止条件会使深度无界。

修复

// ❌ 无环保护
function clone(value) {
  if (typeof value !== 'object' || value === null) return value;
  const out = Array.isArray(value) ? [] : {};
  for (const key of Object.keys(value)) out[key] = clone(value[key]);
  return out;
}

// ✅ 使用 WeakMap 记录已访问对象
function clone(value, seen = new WeakMap()) {
  if (typeof value !== 'object' || value === null) return value;
  if (seen.has(value)) return seen.get(value);
  const out = Array.isArray(value) ? [] : {};
  seen.set(value, out);
  for (const key of Object.keys(value)) out[key] = clone(value[key], seen);
  return out;
}

还可以把尾递归改为循环或显式栈;对不可信输入设置深度上限并返回清晰错误;异步迭代用 await 与定时器让出,而不是递归 Promise 链;图遍历用 WeakSet 打破环。

验证

最小复现不再抛错;循环对象被显式处理;深嵌套输入在预期深度内终止;为原始输入添加回归测试,并在目标浏览器与 Node.js 版本中运行。

增加栈大小只会降低失败频率,不能修复无界遍历。捕获 RangeError 后继续执行可能留下部分处理的数据。