函数重入指未执行完即被再次调用(常因事件/异步触发),递归是函数直接或间接调用自身;区别在于控制流来源——递归同步可预测,重入多源于外部干预。

JavaScript 中函数重入(reentrancy)和递归调用本身不会自动触发堆栈安全检查,语言运行时(如 V8)只在实际调用深度超过引擎限制时抛出 RangeError: Maximum call stack size exceeded。所谓“安全检查”需开发者主动设计,而非语言内置机制。
什么是函数重入?和递归有何区别?
重入指函数在未执行完毕前被再次调用(可能由事件、回调、定时器、异步操作等触发),且上下文可能交错;递归则是函数直接或间接调用自身。关键区别在于控制流来源:递归是同步/可预测的调用链,重入常源于异步或外部干预,更难追踪。
例如:
- 一个事件处理器中修改了全局状态,又触发了同一处理器(如
input事件中更新value导致再次触发)——这是典型重入; - 计算阶乘的
factorial(n)每次调用自身减一 —— 这是标准递归。
如何防止递归导致栈溢出?
对已知可能深度较大的递归,应优先转为迭代实现;若必须递归,可加入显式深度控制:
立即学习“Java免费学习笔记(深入)”;
- 传入当前深度参数,到达阈值立即返回或抛出自定义错误;
- 使用尾调用优化(TCO)写法(注意:仅 Safari 严格支持,Chrome/Firefox 当前不启用);
- 对大数据量场景,改用
setTimeout或queueMicrotask拆分任务,让调用栈有机会清空(即“伪递归”或 trampolining)。
如何识别和防御重入风险?
重入本身不必然危险,但若函数依赖并修改共享状态(如全局变量、DOM、缓存对象),就容易引发竞态或逻辑错乱。防御方式包括:
-
守卫标志(Guard Flag):函数开头检查
if (this._isRunning) return;,执行前设为true,结束时恢复false; - 状态快照 + 差异比对:进入前保存关键状态(如 DOM 值、数组长度),退出后校验是否被意外修改;
-
解耦执行时机:用
Promise.resolve().then(() => {...})将逻辑推到下一个 microtask,避免同步重入; - 避免副作用:将纯计算逻辑与状态变更分离,重入时只重新计算,不重复提交或渲染。
浏览器与 Node.js 的堆栈限制差异
V8 默认栈大小约为 1MB(具体值因版本和平台浮动),对应约 10k–20k 层简单函数调用。可通过启动参数调整(Node.js 中 --stack-size=2048 单位 KB),但无法在浏览器中修改。因此生产代码不应依赖“加深一点也没事”,而应把深度控制作为健壮性边界来设计。
不复杂但容易忽略:重入和递归的真正风险不在“会不会爆栈”,而在“状态是否可控”。栈溢出只是表象,逻辑异常才是核心问题。










