36

Let's say I have something like

function animate(param)
{
    // ...
    if (param < 10)
        setTimeout(function () { animate(param + 1) }, 100);
}

animate(0);

Does this mean each instance of the function's local data will be held in memory until animate completes, i.e. until param reaches 10?

If it's true that instances are held in memory, is there a better way of doing this? I know, passing textual code to setTimeout() solves the problem but in my case there are objects among function arguments that can't be represented as strings easily.

Glen Selle
  • 3,966
  • 4
  • 37
  • 59
mojuba
  • 11,842
  • 9
  • 51
  • 72
  • also consider `setTimeout` allows you to pass additional arguments – `setTimeout(animate, 100, param + 1)` – in which case no closure would need to be made. This is very effective in some scenarios. – Mulan Oct 13 '16 at 20:35

5 Answers5

13

No, at most two instances of function's local data will be held in memory at any given point in time. Here is the order of events:

  1. animate(0) is called.
  2. A closure with param == 0 is created, it now prevents this variable from being released.
  3. Timeout fires, animate(1) is called.
  4. New closure with param == 1 is created, it now prevent this variable from being released.
  5. The first closure finishes executing, at this point it is no longer referenced and can be released. The local variables from the first animate() call can also be released now.
  6. Repeat starting with step 3, now with animate(2).
Wladimir Palant
  • 56,865
  • 12
  • 98
  • 126
  • looks like step 5. is nullified as soon as there is any variable within the outer context and therefore, it's activation object gets filled. – jAndy Aug 11 '11 at 09:57
  • 2
    @jAndy: No, it isn't. The memory can be released at this point - but that doesn't mean that it will be, the exact timing is still up to the garbage collector and memory allocator algorithms. – Wladimir Palant Aug 11 '11 at 10:29
  • Excelent answer, I wanted to write something similar, but now it's not needed :-) – Tomas Aug 11 '11 at 11:53
  • I think the outer closure (`animate`) exits long before the first timeout is fired. Ie, step 5 happens sometime before step 3. No? Of course if this is true, the rest of the steps would have to be re-written too. – Mulan Oct 13 '16 at 20:31
  • @naomik: `animate()` is not a closure but a regular function. There is only one closure in the example - the timeout callback. And - yes, `animate()` finishes executing before the timeout is fired. However, its local variables cannot be released because the closure still exists. Once the timeout fires that closure is released and with it the local variables from the first `animate()` call. But by then a new closure already exists and keeps local variables from the second `animate()` call in memory. – Wladimir Palant Oct 14 '16 at 07:53
  • Hmm difference of vocabulary it seems. A function, named or unnamed, creates a closure. – Mulan Oct 14 '16 at 10:53
  • @naomik: Yes, closures and anonymous functions [aren't the same thing](https://en.wikipedia.org/wiki/Closure_(computer_programming)#Anonymous_functions). Which doesn't change the fact that `animate()` doesn't have an outer scope to capture - no closure. – Wladimir Palant Oct 15 '16 at 21:41
6

Actually, you don't create a recursive function there. By invoking setTimeout its not calling itself anymore. The only closure created here is the anonymous function for setTimeout, once that is executed the garbage collector will recognize that the reference to the previous instance can get cleaned up. This probably will not happen instantly, but you definetly can't create a stack overflow using that. Lets view this example:

function myFunc() {
   var bigarray = new Array(10000).join('foobar');

   setTimeout(myFunc, 200);
}
myFunc();

Watching the memory usage from your browser now. It'll grow constantly, but after a while (20-40 seconds for me) it'll get completely cleaned again. On the other hand, if we create a real recursion like this:

function myFunc() {
   var bigarray = new Array(10000).join('foobar');

   myFunc();
}
myFunc();

Our memory usage will grow, browsers locks down and we finally create that stack overflow. Javascript does not implement Tail recursion, so we will end up with an overflow in all cases.


update

it lookes like I was wrong in my first example. That behavior is only true if no function-context is invoked (using a anonymous function for instance). If we re-write that like

function myFunc() {
   var bigarray = new Array(10000).join('foobar');

   setTimeout(function() {
     myFunc();
   }, 200);
}
myFunc();

Browser memory seems not to get released any more. Grows forever. That might be because any inner-closured keeps a reference to bigarray. But anyway.

jAndy
  • 231,737
  • 57
  • 305
  • 359
  • 1
    According to `about:memory` in the current Firefox nightly the memory is being released - but not all of it is being released to OS, probably due to memory fragmentation. This is definitely an unrelated problem and if you frequently create large objects you get there regardless of closures. – Wladimir Palant Aug 11 '11 at 10:28
4

Only the context related to the most recent call to animate will be retained (in theory, it's all down to the garbage collector). When animate creates the anonymous function, the anonymous function gets a reference to the context of that call to animate and so that context remains in memory. When the timeout occurs, the timer code's reference to the anonymous function is released, which releases that function's reference to the context. A new context has meanwhile been created, but the new context doesn't reference the old one, so it won't keep the old one in memory.

The key to this is that the binding of the context to the function happens when the function is created, not when it's called.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @TJCrowder: seems only to work if there are no properties on the activation object from the outer scope. Have a look at my last example. on FF5 Mac (and win) memory is never released. – jAndy Aug 11 '11 at 10:09
  • @jAndy: Remember that just because memory *can* be reclaimed, that doesn't mean the browser engine is in any hurry to actually do it. Check out [this example](http://jsbin.com/ugohug). That very quickly achieves steady-state for me on Chrome, and very near the starting memory value for the page, because Chrome has a fairly proactive memory manager. On Firefox 3.6, memory use approximately doubles before returning to very near the starting point, presumably because Firefox 3.6's memory manager waits until it really feels it needs to before doing a reclamation. – T.J. Crowder Aug 11 '11 at 10:54
2

Yes, in your example, each time the animate function is executed a new function is created with its own closure scope.

To avoid the multiple closures, you could try something like this:

function animate (param) {
    function doIt () {
        param++;
        if (param < 10) {
            setTimeout(doIt, 100);
        }
    };
    setTimeout(doIt, 100);
}
nickf
  • 537,072
  • 198
  • 649
  • 721
  • 1
    But in the OP's original code, the context related to earlier calls is no longer referenced once the timer related to them has fired, so they're free to be GC'd. E.g., the second time the timer fires, the context of the first call is no longer referenced by anything. – T.J. Crowder Aug 11 '11 at 09:34
  • @T.J. Crowder, it was my reading of the question that this was actually desirable. – nickf Aug 11 '11 at 09:52
  • Yes, that was mine as well. I'm not seeing why it needs recasting (not that there's anything wrong with how you recast it. – T.J. Crowder Aug 11 '11 at 10:41
  • or just ... `setTimeout(animate, 100, param + 1)` – which uses *zero* additional closures. – Mulan Oct 13 '16 at 20:36
-2

How about

function animate(param)
{
  //....
  if(param < 10)
    animateTimeout(param);
}

function animateTimeout(param)
{
   setTimout(function() { animate(param + 1) }, 100 );
}

This way you aren't storing the local data in //... while waiting for the timeout.

Not sure if you're thinking there is more of a problem here than there really is however. Your code isn't recursive in that it will result in 10 deep chain of closures because closure 1 is exited once the second call is made to animate. Each closure only exists for the lifetime of one setTimeout.

James Gaunt
  • 14,631
  • 2
  • 39
  • 57
  • Then instances of animateTimout will be stuck in memory, no? – mojuba Aug 11 '11 at 09:24
  • No, the first instance is free to be garbage collected once the code in setTimeout is executed - because once the code in setTimeout is executed and returns nothing remains to reference the locals in the first call to animate. – James Gaunt Aug 11 '11 at 09:29
  • Oh sorry - in answer to animateTimeout - yes - but again for only 1 iteration. And I'm assuming there is more local data in your commented section that you wanted to avoid storing. If you're worry is about the 10 deep problem then ignore this code as that's not happening anyway. – James Gaunt Aug 11 '11 at 09:32