Taming the Javascript event scope: Closures

When doing client-side developing there are times that jQuery’s get-this-do-that nature doesn’t provide all that is needed. For more complex applications I usually find myself creating javascript objects that ‘control’ a specific part of the page’s interaction. In the objects the application’s state is tracked, references to other objects (could be relevant DOM nodes) are stored and event handlers are set.

One of the problems typically encountered when dealing with javascript event handlers is that they have their own take on the ‘this’ keyword. Closures to the rescue.

A simple (and rather useless) example:

html:

</head>



<div id="scope">
  <div>
    <p>
      
    </p>
            
    
    <input type="button" value="btn 1" />
        
  </div>
      
  
  <div>
    <p>
      
    </p>
            
    
    <input type="button" value="btn 2" />
        
  </div>
  
</div>

javascript:

function controllerExample(node,idx)
{
    this.node = node;
    this.idx = idx;
    this.clickCount = 0;

    this.setEventHandlers();
}
controllerExample.prototype.setEventHandlers = function()
{
      $(this.node).find('input').bind('click',this.handleClick);
}
controllerExample.prototype.handleClick = function(event)
{
    this.clickCount++;
    $(this.node).find('p').text(
        'Button is clicked '+this.clickCount+' time(s)'
    );
}

‘this’ is wrong

Now that doesn’t work… When the click event fires the handleClick() function is executed but the scope is not the controllerExample instance. On execution the ‘this’ keyword points to the input element. So we see ‘Button is clicked NaN time(s)’.

The Prototype javascript library has a solution for this by extending the basic function object type with the bind() and bindAsEventListener() methods. Very nice but to include Prototype just for that… bad idea. It’s perfectly possible to separately implement a similar bind() function method but that still creates an additional (besides jQuery) dependency for that method. Let’s keep the mess to a minimum and keep it ‘in’ the controllerExample object.

Closure

Prototype’s bind() function uses what is called a closure: A function defined within a function. The benefit is that the inner function, which in the following example is returned by the outer function, has access to the outer functions local variables after the outer function has returned.

Properly ‘binding’ the handleClick event handler now looks like this:

controllerExample.prototype.setEventHandlers = function()
{
    var _scope = this;
    var getHandleClick = function() {
        console.log(this); // window
        console.log(_scope); // controllerExample
        return function(event) {
            return _scope.handleClick.call(_scope,event);
        }
    }
    $(this.node).find('input').bind('click',getHandleClick());
}

getHandleClick() returns a function that, when executing, still has access to _scope which is the correct scope. The console.log lines are there to illustrate that inside the inner function we can’t use ‘this’. The getHandleClick() function has no object scope so it’s ‘this’ is the window scope. But the object scope can be passed into by copying ‘this’ to a new variable _scope and use that inside the closure.

More information about closures: Javascript closures 101 (basic) and This article on jibbering.com (advanced).

routeEvent

Now the above is fine if there’s just one or two event handlers to be added but is not really ‘generic’. So let’s create a more generic ‘routeEvent’ method that creates a closure for the eventHandler that is passed in:

controllerExample.prototype.setEventHandlers = function()
{
    $(this.node).find('input').bind(
        'click',
        this.routeEvent(this.handleClick)
    );
}
// ...
controllerExample.prototype.routeEvent = function(eventHandler)
{
    var _scope = this;
    return function(event) {
        return eventHandler.call(_scope,event);
    }
}

Now the handleClick() method will be called with the correct ‘this’ and is also provided with the jQuery event object.

A demo putting it al together can be seen here.