JavaScript/ECMAScript 垃圾回收

3
考虑以下代码(您可以将其放入Chrome开发者控制台中进行检查):
var obj = {
    f: function () {
        var myRef = this;
        val = setTimeout(function () { 
            console.log("time down!"); 
            myRef.f();
        }, 1000);
    }
};

如果我运行以下命令:
obj.f();

要启动计时器,我可以每秒看到“时间倒计时!” 如果我运行
obj = null;

计时器仍在运行。
我很好奇为什么垃圾回收不能清除计时器?可怕的是,似乎现在没有办法删除计时器 - 我是正确的吗?
我的猜测是,从技术上讲,window 仍然持有对对象的引用,因此对象仍然留在内存中。我在另一种基于 ECMA 的语言(Actionscript)中遇到过这个问题,并构建了一个处理它的库,但我认为 Javascript 会采取不同的方法。

值得注意的是,查看以下问题的答案:https://dev59.com/P3RA5IYBdhLWcg3wsgPq 好像没有办法停止计时器! - Mark Rhodes
1
这并不是需要特殊处理的问题 - 这是设计上的。如果您打算在用户导航离开页面之前停止计时器,则保存setTimeout的返回值,而不是将其丢弃,以便您可以使用clearTimeout来停止它。 - Jamie Treworgy
你在实际代码中不会真的使用任何看起来像这样的东西吧?这种内联函数声明、容器对象和从一个作用域泄漏到另一个作用域的整个组合,都使得代码非常难以阅读。有一些情况下,一些简单的封装可以解决一些问题,但这似乎不是其中之一。 - aaaaaaaaaaaa
当然我不会这样做,但考虑一些使用自己的“计时器”库...然后考虑有多少其他程序员会使用$("div").html("")来“清除”它,而不是使用某个图表的“销毁”方法。这些计时器可能没有被销毁...我希望能在一个div上设置一个setTimer...然后如果div被清除了,计时器也会被清除...这可能会减少应用程序中的内存泄漏。 - K2xL
4个回答

7
obj没有被垃圾回收,因为你传递给setTimeout的闭包必须保留以便执行。而且,它又持有对obj的引用,因为它捕获了myRef

如果您将该闭包传递给任何其他保留它的函数(例如在数组中),那么它将是相同的情况。

现在没有办法删除定时器,除非使用可怕的hack方法1。但这很自然:一个对象的工作是在自己之后进行清理。这个对象的目的是无限地触发一个超时,因此该对象显然打算永远不会在自己身后进行清理,这可能是合适的。你不能期望一些事情一直发生而不使用至少一些内存来完成它。


1 可怕的hack方法:由于计时器ID只是整数,所以您可以从1到1000000000循环,并在每个整数上调用clearTimeout。 显然,这将杀死其他正在运行的定时器!


你怎么知道是setTimeout()阻止了垃圾回收?可能是console.log(),如果你移除console.log(),它就会被垃圾回收。 - James

1
  • 列表项

当然,计时器仍然会触发;你在嵌套函数中使用myRef.f递归调用它。

你猜测窗口保存了对obj的引用。这是正确的,但这不是setTimeout递归调用的原因,也不能取消它。

有几种方法可以提供计时器清除功能。其中一种方法是在开始时传入一个条件函数。

要停止计时器,只需调用clearTimeout,然后不再递归调用setTimeout。一个基本示例:

(标识符val被创建为全局对象的属性。始终使用var!)

var obj = {
    f : function (i) {
        // (GS) `this` is the base of `f` (aka obj).
        var myRef = this;
        var timer = setTimeout(function () { 
            if(i == 0) {
                clearTimeout(timer);
                return;
            }
            console.log(i, "time down!"); 
            myRef.f(--i);
        }, 1000);
    }
};
obj.f(4);

从那里向上移动一步,isDone方法可以提供更多功能的检查,并将引用传回和传递。setTimeout可以更改为setInterval。

var obj = {
    f : function (i, isDone, animEndHandler) {
        var timer = setInterval(function() { 
            console.log(i, "倒计时结束!"); 
            if(isDone(--i)) {
                animEndHandler({toString: function(){return"发射!"}, i: i});  
                clearInterval(timer);
            }
        }, 1000);
    }
};
function isDone(i) { return i == 0; }
function animEndHandler(ev) { console.log(""+ev); } obj.f(3, isDone, animEndHandler);

1
作为对K2xL评论的回应。
稍微调整一下您的函数,它就会像您所建议的那样运行。如果给obj赋一个新值,if将失败,传播将停止,整个过程可以被垃圾回收:
var obj = {
    f: function () {
        var myRef = this;
        if(myRef===obj){
            val = setTimeout(function () { 
                console.log("time down!"); 
                myRef.f();
            }, 1000);
        }
    }
};

我更喜欢稍微扁平化的结构,你可以跳过对象容器,仅依赖于标准闭包:

(function(){
    var marker={}
    window.obj=marker
    function iterator(){
        if(window.obj===marker){
            setTimeout(iterator,1000)
            console.log("time down!")
        }
    }
    iterator()
})()

请注意,您可以使用任何您想要的标记对象,这可以很容易地成为文档元素。即使在其位置上建立了具有相同ID的新元素,当该元素从文档中删除时,传播仍将停止,因为新元素与旧元素不相等:
(function(){
    var marker=document.getElementById("marker")
    function iterator(){
        if(document.getElementById("marker")===marker){
            setTimeout(iterator,1000)
            console.log("time down!")
        }
    }
    iterator()
})()

0
垃圾收集器不清除计时器功能,因为setTimeout()实现中的某些东西会维护对它的引用,直到调用clearTimeout()
如果您不清除它并且丢弃由"setTimeout()"返回的值的引用,则引入了"内存泄漏"(即计时器函数无法被移除),您是正确的。

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