Thursday 6 June 2013

Javascript Jedi: Getting Closure

I'm often stunned by the number people who still dont get closures... but then part of that is that closures often get confused with other Javascript goodness. Closures are used everywhere in javascript: callbacks, event handlers, private functions, you name it... they are literally all over the place. Getting a good handle on what exactly a closure is will help you understand how to use it and make a lot of the texts on javascript make a lot more sense.

So for anyone who doesn't fully get closures or wants to give a short explanation to someone else who doesn't get them, here is my take of what a closure is in it's most basic terms.

Defining a closure is actually much simpler than people think:
A closure is when one function (e.g. foo() ) is defined within the body of another function (e.g bar() ), and the inner function (foo) uses variables in the local scope of the outer function (bar).
For example:

function bar(a) {
    var msg = a;
    function foo() {
        alert(msg);
    }
    foo();
}
bar("HELLO");
bar("G'DAY");

Here we can see foo(), existing entirely inside the function bar(), and using the variable msg which exists only in the scope of the parent function.

A closure like this could be used to contain code that needs to be in a function but that you dont want shared. One common use for this exact syntax is to get around the performance hit of putting resource intensive code inside a try catch:

function bar(count){
    function foo() {
        for (var i = 0; i < count; i++) {
            /*
            Doing lots of variable assignments here!
            */
        }
    }
    try {
        foo();
    }
    catch (e) {
        // Handle your errors
    }
}
bar(1000);

In the above example a performance critical loop has been wrapped in a closure to allow the script engine to optimise it for speed (this is a bigger concern on some browser engines than others).

Far more common however is the use of closures in handling events or callbacks.

The simplest example of a callback is setTimeout() where a function name or reference is passed along with a number of milliseconds to wait before execution of that function.

function bar(a) {
    var msg = a;
    function foo() {
        alert(msg);
    }
    setTimeout(foo, 3000); // Wait 3 seconds and then run foo()
}
bar("HELLO");
bar("G'DAY");

In this case the setTimeout() calls the function foo() after waiting 3 seconds, but each call is a separate instance of bar with it's own separate instance of msg. Each closure is therefore also separate and retains the parent scope of when it was created.

The result will be two alerts "HELLO" and "G'DAY".

This could also be written using a nameless (or anonymous) function as follows:

function bar(a) {
    var msg = a;
    setTimeout(function() {
        alert(msg);
    }, 3000);
}

However... It's important to realise that a closure is NOT a snapshot of the scope. Any code put in a callback to be executed later uses the current value of a variable. For instance in the above case, if msg had been redefined before the timeout the new value would be used not simply the value of msg at the time the closure was defined.

function bar(a, b) {
    var msg = a;
    function foo() {
        alert(msg);
    }
    setTimeout(foo, 3000); 
    msg = b;
    setTimeout(foo, 3000); 
}
bar("HELLO", "G'DAY");

This example will alert "G'DAY" twice, because that is the value of msg when the both timeouts are executed.

This is important to remember when adding closures as callbacks from inside loops!

function bar(count) {
    for (var i = 0; i < count; i++) {
        setTimeout(function() {
            alert(i);
        }, 3000);
    }
}
bar(100);

All you will get here is the final value of i (100) alerted one hundred times because each callback is using the same i.

There are several ways around this but in essence all of them work by copying the variables in question out of the scope of the parent so they are not shared between callbacks.

function wrap() {
    // Convert arguments to true array
    var args = Array.prototype.slice.call(arguments, 0);
    return function() {args.shift().apply(arguments.callee.caller, args);}
}

function bar(count) {
    var msg = "Hi";
    for (var i = 0; i < count; i++) {
        setTimeout(wrap(function(x) {
            alert(msg + "-" + x + "-" + i);
        }, i), 3000);
    }
}
bar(10);

Here you pass the closure along with any variables you want to "snapshot" out of active scope to the wrap function. This then passes back another closure, however because each call to this function is a new instance of it, any arguments passed exist in their own independent scope.

You'll notice that both the msg and i variables are still from the original scope and are unaffected.

So thats my explanation of closures and my best tip for getting around their main limitation. You just need to keep in mind what scope a closure is operating in to make the best use of them.

No comments:

Post a Comment