本文介绍如何计算 JavaScript 引擎支持的最大调用栈深度,并分析 ECMAScript 6 中尾递归优化对栈大小的影响。通过示例代码对比不同引擎下的栈帧限制,探讨尾递归在严格模式下的行为及常见实现误区。
JavaScript 调用栈深度计算方法
当编写一个无限递归自身的死循环函数时,程序会迅速抛出“Uncaught RangeError: Maximum call stack size exceeded”错误。这引发了一个疑问:JavaScript 引擎允许的最大调用栈深度具体是多少?
1. 计算方法
可以通过以下代码测试当前 JavaScript 引擎支持的最大递归深度,该方法灵感来源于 Ben Alman 的代码片段:
function computeMaxCallStackSize() {
try {
return 1 + computeMaxCallStackSize();
} catch (e) {
// Call stack overflow
return 1;
}
}
在不同环境中运行上述代码,可获得如下结果:
- Node.js: 11034
- Firefox: 50994
- Chrome: 10402
这些数值代表什么含义?据 Mr.Aleph 解释,在 V8 引擎中,可递归调用的层数取决于两个因素:1. 栈的总大小;2. 每个栈帧的大小(用于存储函数参数和局部变量)。如果在 computeMaxCallStackSize 函数中声明局部变量,会发现最大递归深度数值变小。
ES6 尾递归优化机制
2. ECMAScript 6 中的尾递归优化
ECMAScript 6 标准引入了尾递归优化机制:若函数的最后一个操作是函数调用,则引擎会采用“跳转”而非“子调用”的方式执行。这意味着,若将 computeMaxCallStackSize 重写为尾递归形式,并在 ES6 严格模式下运行,该函数将无限执行而不会栈溢出。
function computeMaxCallStackSize(size) {
size = size || 1;
return computeMaxCallStackSize(size + 1);
}
尾递归实现的常见误区
3. 相关评论分析
Andrei 指出:“ECMAScript 6”版本的代码实际上无法正常运行。虽然 size 变量会递增,但最终没有返回值。
回复 Andrei:确实如此。这段代码不能用于计算栈大小。在 ES6 环境下,该代码会无限执行,因此不会返回数据;而在其他情况下,则会抛出 RangeError。为了使代码正常工作,我对代码进行了重写:
var computeMaxCallStackSize = (function() {
return function() {
var size = 0;
function cs() {
try {
size++;
return cs();
} catch(e) {
return size + 1;
}
}
return cs();
};
}());