3

Consider the following code (you can just put this in the developer console in Chrome and check).

var obj = {
    f: function () {
        var myRef = this;
        val = setTimeout(function () { 
            console.log("time down!"); 
            myRef.f();
        }, 1000);
    }
};

If I then run

obj.f();

to start the timer, I can see every second "time down!"

If I then run

obj = null;

The timer still fires.

Just curious why doesn't garbage collection clear out the timer? The scary thing is that it appears that there is no way to delete the timer now - am I correct?

My guess is that technically window still holds a reference to the object still consequently the object stays in memory. I've experienced this problem in another ECMA based language (Actionscript) and built a library for handling it, but sort of thought Javascript would take a different approach.

Domenic
  • 110,262
  • 41
  • 219
  • 271
K2xL
  • 9,730
  • 18
  • 64
  • 101
  • It might be worth checking out the answers to: http://stackoverflow.com/questions/858619/viewing-all-the-timouts-intervals-in-javascript it seems there is no way to stop the timer when it's done like this! – Mark Rhodes Jan 12 '12 at 21:18
  • 1
    This isn't exactly a problem that requires special handling - this is by design. If you intend to stop a timer before the user navigates away from the page, then save the return value from `setTimeout` instead of throwing it away, so you can use `clearTimeout` to stop it. – Jamie Treworgy Jan 12 '12 at 21:23
  • You wouldn't actually happen to use anything that looks like this in real code? This whole combination of inline function declarations, a container object and leaking from one scope to another, it all makes the code quite hard to read. There are a few situations where a little packaging can solve some problems, but this doesn't seem like one of them. – aaaaaaaaaaaa Jan 12 '12 at 21:39
  • @eBusiness of course i wouldn't, but consider some libaries that use their own "timers" ... then consider how many other coders do $("div").html("") to "clear" it out instead of using some chart's "destroy" method. those timers might not be destroyed... i wish one could put a setTimer on a div... then if the div is cleared the timer is also cleared... could lead to less memory leaks in apps – K2xL Jan 16 '12 at 04:47

4 Answers4

7

obj is not garbage collected because the closure that you pass to setTimeout must be kept around in order to be executed. And it, in turn, holds a reference to obj because it captures myRef.

It would be the same if you passed that closure to any other function that kept it around (for example in an array).

There is no way to delete the timer now, without horrible hacks1. But this is pretty natural: it's an object's job to clean up after itself. This object's purpose is to infinitely fire a timeout, so that object clearly intends to never clean up after itself, which might be appropriate. You can't expect something to happen forever without using up at least some memory while it does so.


1 Horrible hack: since timer IDs are just integers, you can loop from, say, 1 to 1000000000 and call clearTimeout on each integer. Obviously this will kill other running timers!

Domenic
  • 110,262
  • 41
  • 219
  • 271
  • How do you know that it is setTimeout() that is preventing garbage collection? It might be console.log(), and it might garbage collect if you remove the console.log(). – James Sep 03 '14 at 18:49
1
  • List item

Of course the timer still fires; you're recursively calling it inside the nested function with myRef.f.

Your guess was that the window holds a reference to obj. That is true, however, that's not why setTimeout is recursively called, nor what can be done to cancel it.

There are a few ways to provide for timer clearing. One way would be to pass in a condition function in the beginning.

To stop the timer, merely call clearTimeout and then don't recursively call setTimeout. A basic example:

(Identifier val is created as a property of the global object. Always use 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);

Moving a step up from that, an isDone method can provide more featureful check with refs passed back an forth. The setTimeout can be changed to setInterval.

var obj = {
    f : function (i, isDone, animEndHandler) {
        var timer = setInterval(function() { 
            console.log(i, "time down!"); 
            if(isDone(--i)) {
                animEndHandler({toString: function(){return"blast off!"}, i: i});  
                clearInterval(timer);
            }
        }, 1000);
    }
};

function isDone(i) {
  return i == 0;
}

function animEndHandler(ev) {
  console.log(""+ev);
}
obj.f(3, isDone, animEndHandler);
Garrett
  • 2,936
  • 1
  • 20
  • 22
1

In response to K2xL's comment.

A minor adjustment of your function and it does behave like you suggest. If obj is given a new value the if will fail, the propagation will stop, and the whole lot can be garbage collected:

var obj = {
    f: function () {
        var myRef = this;
        if(myRef===obj){
            val = setTimeout(function () { 
                console.log("time down!"); 
                myRef.f();
            }, 1000);
        }
    }
};

I'd prefer a slightly flatter structure, you can skip the object container and rely just on a standard closure:

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

Note that you can use any object you desire for marker, this could easily be a document element. Even if a new element with the same id is erected in its place the propagation will still stop when the element is removed from the document as the new element is not equal to the old one:

(function(){
    var marker=document.getElementById("marker")
    function iterator(){
        if(document.getElementById("marker")===marker){
            setTimeout(iterator,1000)
            console.log("time down!")
        }
    }
    iterator()
})()
aaaaaaaaaaaa
  • 3,630
  • 1
  • 24
  • 23
0

The garbage collector doesn't clear out the timer function because something in the implementation of setTimeout() maintains a reference to it until you call clearTimeout().

You are correct that if you do not clear it and drop the reference to the value returned by "setTimeout()" then you have introduced a "memory leak" (in that the timer function cannot be removed).

maerics
  • 151,642
  • 46
  • 269
  • 291