JavaScript中的调用栈大小

45
我想测试大型调用栈。具体来说,我希望在调用栈长度达到1000时收到控制台警告。这通常意味着我做了一些愚蠢的事情,并可能导致微妙的错误。
我能否在 JavaScript 中计算调用栈长度?

2
这个链接是否有所帮助? - Dave Newton
错误堆栈跟踪只提供最多10个堆栈条目,不是吗?http://jsfiddle.net/pimvdb/AuyP7/ - pimvdb
2
在某些情况下,您可以使用 console.trace() - Poelinca Dorin
1
这通常不可能在没有arguments.calleecaller的情况下完成。请参见我的修订答案。 - Tim Down
请注意,由于其年龄,它针对的是Opera和IE的旧JavaScript引擎,至少在Opera的情况下,所有这些属性都发生了根本性的变化。 - gsnedders
显示剩余2条评论
3个回答

51

这是一个在所有主要浏览器中都可以使用的函数,但在ECMAScript 5严格模式下无法使用,因为arguments.calleecaller已在严格模式中删除。

function getCallStackSize() {
    var count = 0, fn = arguments.callee;
    while ( (fn = fn.caller) ) {
        count++;
    }
    return count;
}

例子:

function f() { g(); }       
function g() { h(); }       
function h() { alert(getCallStackSize()); }       

f(); // Alerts 3

2011年11月1日更新

在ES5严格模式中,根本无法遍历调用堆栈。唯一的选择是解析new Error().stack返回的字符串,这是非标准的,不被普遍支持和明显存在问题,而且可能永远无法实现

2013年8月13日更新

该方法也受到限制,因为在单个调用堆栈中多次调用的函数(例如通过递归)将使getCallStackSize()陷入无限循环(由评论中的@Randomblue指出)。改进版本的getCallStackSize()如下:它跟踪它之前看到的函数以避免进入无限循环。然而,返回的值是在遇到重复之前调用堆栈中不同函数对象的数量,而不是完整调用堆栈的真实大小。不幸的是,这是你所能做的最好的了。

var arrayContains = Array.prototype.indexOf ?
    function(arr, val) {
        return arr.indexOf(val) > -1;
    } :
    function(arr, val) {
        for (var i = 0, len = arr.length; i < len; ++i) {
            if (arr[i] === val) {
                return true;
            }
        }
        return false;
    };

function getCallStackSize() {
    var count = 0, fn = arguments.callee, functionsSeen = [fn];

    while ( (fn = fn.caller) && !arrayContains(functionsSeen, fn) ) {
        functionsSeen.push(fn);
        count++;
    }

    return count;
}

9
不错的解决方案。只是以防万一有人遇到这个问题:在Chrome开发者工具中,它会弹出6,但那是因为使用控制台时似乎还执行了3个其他函数。 - pimvdb
不错!所以,在ES5严格模式下,他们决定调用堆栈太危险了?也许这只是为了防止更改调用堆栈的默认行为。 - Frederik.L
@Fred,当你想要进行函数内联和尾调用优化(这在ES.next中是强制性的)时,arguments.caller/arguments.callee变得非常有趣。 - gsnedders
1
这在带有循环的调用堆栈中无效。在这种情况下,函数 getCallStackSize 不会返回。 - Randomblue
@Randomblue:示例脚本? - Tim Down
@Randomblue:说得好。问题在于函数对象只有一个caller属性,因此如果一个函数调用了自身,那么它的caller属性就是对自身的引用,因此while循环永远不会退出。你可以通过将fnfn.caller进行比较来检查这一点(http://jsfiddle.net/3DNUw/2/),但返回的调用堆栈大小不再准确。此外,这也无法处理递归调用链的情况(http://jsfiddle.net/3DNUw/3/)。解决这个问题的方法是保持已经看到的函数列表,但调用堆栈大小仍然可能不准确。 - Tim Down

2

另一种方法是在顶层堆栈帧中测量堆栈上的可用空间,然后通过观察可用空间减少的数量来确定堆栈上已使用的空间。 代码如下:

function getRemainingStackSize()
{
    var i = 0;
    function stackSizeExplorer() {
        i++;
        stackSizeExplorer();
    }

    try {
        stackSizeExplorer();
    } catch (e) {
        return i;
    }
}

var baselineRemStackSize = getRemainingStackSize();
var largestSeenStackSize = 0;

function getStackSize()
{
    var sz = baselineRemStackSize - getRemainingStackSize();
    if (largestSeenStackSize < sz)
        largestSeenStackSize = sz;
    return sz;
}

例如:

function ackermann(m, n)
{
    if (m == 0) {
        console.log("Stack Size: " + getStackSize());
        return n + 1;
    }

    if (n == 0)
        return ackermann(m - 1, 1);

    return ackermann(m - 1, ackermann(m, n-1));
}

function main()
{
    var m, n;

    for (var m = 0; m < 4; m++)
    for (var n = 0; n < 5; n++)
        console.log("A(" + m + ", " + n + ") = " + ackermann(m, n));
    console.log("Deepest recursion: " + largestSeenStackSize + " (" +
            (baselineRemStackSize-largestSeenStackSize) + " left)");
}

main();

当然,这种方法有两个主要的缺点:
(1) 当虚拟机具有大的堆栈大小时,确定已使用的堆栈空间可能是一项潜在的昂贵操作。
(2) 报告的数字不一定是递归的数量,而是实际在堆栈上使用的空间的测量值(当然,这也可以是一个优势)。我曾经看到自动生成的代码,其中包含使用与上面的stackSizeExplorer函数的2000次递归相同的堆栈空间的函数。
注意:我只在node.js上测试过上述代码。但我认为它将适用于所有使用静态堆栈大小的虚拟机。

2
您可以使用此模块: https://github.com/stacktracejs/stacktrace.js 调用printStackTrace返回一个数组内的堆栈跟踪信息,然后您可以检查其长度。
var trace = printStackTrace();
console.log(trace.length());

最佳解决方案。谢谢这个:D 但我使用了这段代码: require('stacktrace-js').getSync().length - Renato Cassino

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接