Feb 8 2010

Scope problems with JavaScript setInterval & setTimeout? Use closures!

Category: NovoGeek @ 21:15

JavaScript closures is one of those topics which, I always thought, is beyond my head. Though several articles explain the concept, somehow, I was often confused.

I was working on a thick client application, which heavily uses lot of jQuery and JavaScript. I need a client side timer to run periodically, to keep user's session alive and so I was using JavaScript's native "setInterval()" method for my timer. Everything was fine, till I suddenly faced a weird problem.

Suppose, you have created a  global function which is to be fired periodically. In simple JS, it can be written as:

var timer=function (){ 
    console.log('my timer'); 
}; 
var timerId=setInterval('timer();',1000); 

This works perfectly well. Here the function "timer()" is at a window level and has global scope. So no issues so far.

However, to clean up the code, if you follow any of these patterns,  you will have good code, but setInterval will not work! See the below code for more clarity:

var TestObject=function(){ 
    var timerId; 
    this.timer=function(){ 
        console.log('test timer'); 
    }; 
    this.startTimer=function(){ 
        timerId=setInterval(function(){ 
            this.timer(); 
        },1000); 
    }; 
    this.stopTimer=function(){ 
        clearInterval(timerId); 
    }; 
}; 
 
var obj=new TestObject(); 
obj.startTimer(); 
obj.stopTimer(); 

In the above code, TestObject is the class which is holding the entire code, so that all my functions are not exposed to global window object. "timer()" is a public function which will be fired periodically and "startTimer()" is the function which will trigger the timer.

If you run the above code (you can quickly try in firebug console), you will get a JavaScript error - "this.timer is not a function". The reason is, setInterval() function will take the scope to window object and this will check for "timer()" at window level. Since you are wrapping it in "TestObject", "timer()" will not be found. To solve this scope problem, you need to use closures and store the instance of the class. So the "startTimer()" function should be like:

this.startTimer=function(){ 
      var inst=this; 
      console.log('startTimer this: ',this); 
      timerId=setInterval(function(){ 
          console.log('setInterval this: ',this); 
          inst.timer(); 
      },1000); 
}; 

The above code has two console statements. The “this” keyword in the first console statement prints the instance of TestObject, while the “this” keyword in setInterval function prints “window” object. This shows that setInterval function will take the scope to window level and hence it is not able to find “this.timer” in the first snippet.

Notice that we are storing the instance of TestObject class in a local variable "inst". Also notice that setInterval is using an anonymous function, which is forming a closure. In this anonymous funtion, we are able to access timer() function using "inst.timer()". This would solve the scope problem! setInterval function will be called for every 1 second and it will always have access to “inst.timer()”. This is the power of closures.

To explain closures in my words,  when there are nested functions, the child function will have access to local variables of parent function(which is as expected). But after the parent function has executed, the child function, when called explicitly, will still have access to the local variable of parent function. JavaScript garbage collector will not clear the value of the variable.

For a graphical explanation of JavaScript closures in jQuery context, please check BenNadel's blog. Ben explains the concept of closures very clearly in this article.

Happy coding :)

Tags:

blog comments powered by Disqus