Closures and Scoping in Javascript
Per Wikipedia, a Closure is:
… a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables) of that function.
I want you to pay special attention to the last part of that definition: together with a referencing environment, containing references to each of the non-local variables of that function. This is easier explained with a little code:
function closureTest() {
var outerVar = "World";
document.onload = function() {
console.log("Hello" + outerVar);
}
}
In this example, the referencing environment corresponds to the outerVar variable, and the closure is the function that’s assigned to the document.onload event property. The result of this example is a “Hello World” message printed in the console when the document is loaded.
You might not be impressed by any of this, as this is a pretty trivial example. The interesting thing to understand about closures lies not so much in the ability to treat functions as variables, but rather in the context (or referencing environment) that these functions can reference in a future point in time.
A True Story about Closures
The other day, as I was pair programming with my coworker, we ran into an issue which led us to a deeper understanding of how Javascript handles closures . The task at hand was a mini library for managing analytics events for a HTML5 mobile app. Each analytics event is triggered by a DOM event (e.g. click) and can have 1 or more attributes associated with it. For example, these are the attributes associated with the modelTap event:
{
'navigationCategory' : 'popular',
'model' : 'myModel'
}
Simple stuff. The tricky part is that, different events may contain different type of attributes, and these attributes need to be populated from different parts of the DOM. We solved this is by creating an array of analytic configuration objects, containing the following properties:
- eventName, a String,
- a CSS selector, a String,
- and a getAttributes Function that returns a Javascript object with the attributes for this event.
For example:
var analyticConfigs = [
{
eventName: 'modelTap',
selector: 'li.model',
getAttributes: function(context) {
var model = $(context).text();
var navigationCategory = $(context).find('#category').text();
return {model: model, navigationCategory: navigationCategory};
}
},
{
eventName: 'navTap',
selector: 'li.navigation_tab',
getAttributes: function(context) {
var tabName = $(context).text();
return {tabName: tabName};
}
}
];
The idea is to iterate through the configuration array and for each config object attach a ‘click’ event handler to the elements resolved by the selector property. Within the click handler, we would call the function that’s responsible for resolving the event name and
attributes object and send this info through the analytics API.
To put it in code:
for (var i = 0; i < analyticConfigs.length; i++) {
var config = analyticConfigs[i];
$('body').on('click', config.selector, function() {
sendAnalyticsEvent(this, config);
})
}
function sendAnalyticsEvent(context, config) {
var eventName = config.eventName;
var attributes = config.getAttributes(context);
// Omitting API call to analytics code for clarity.
console.log("Sending analytic event: " + eventName + "; with attributes " + JSON.stringify(attributes));
}
This should work fine, right ? Turns out it doesn’t. After doing a couple of clicks to the different elements, the output of this is:
> Sending analytic event: navTap; with attributes {"tabName":"Super Tab"}
> Sending analytic event: navTap; with attributes {"tabName":"Super Tab"}
Can you guess where the problem is ?
The problem lies in the for loop. Turns out, Javascript creates new scopes within functions (i.e. private variables). However, it does not do so within blocks (i.e. for and while loops). This means that in the example above, there’s a single config var created for the entire duration of the loop (and beyond!), and that every closure created within the loop, will reference the same config object.
To make this clearer, we can print the contents of the config object outside the loop:
for (var i = 0; i < analyticConfigs.length; i++) {
var config = analyticConfigs[i];
$('body').on('click', config.selector, function() {
sendAnalyticsEvent(this, config);
})
}
console.log("Config: " + config.eventName);
The code above prints:
> Config: navTap
Sick, right ?
So, how to solve this?
The key is to create a new scope for the config variable. As I mentioned previously, Javascript creates a new scope each time a function is created, so we can leverage this in the following way:
for (var i = 0; i < analyticConfigs.length; i++) {
var config = analyticConfigs[i];
$('body').on('click', config.selector, function(cfg) {
return function() { sendAnalyticsEvent(this, cfg) };
}(config));
}
What we’re doing here is wrapping our function within an anonymous function that receives a cfg parameter, effectively creating a new scope (where the cfg var lives in). We immediately call this function passing in our config variable.
The output of this code is now:
> Sending analytic event: modelTap; with attributes {"model":" Super Model ","navigationCategory":""}
> Sending analytic event: navTap; with attributes {"tabName":" Super Tab "}
Interesting, right ?
Conclusion
Lots of languages have closures: obviously functional languages such as Scala, Haskell, Clojure, etc, but also Object Oriented languages such as Ruby, Python, and C#. Even in Java you can achieve similar functionality with anonymous inner classes. In all of these languages though, new scopes are created within blocks such as for and while loops.
Take as an example the following Ruby code:
closure_array = []
numbers = %w{one two three}
numbers.each do |n|
closure_array << ->{puts n}
end
closure_array.each do |c|
c.call
end
The output of the above code is:
> one
> two
> three
As you can see, things work as expected, and there’s no need to create a new scope or anything like that.
Javascript is a very powerful language and it’s far from perfect. In my opinion, this is a flaw of the language itself. Of course, you can argue that it’s just a matter of understanding the language, but still you have to accept that it’s counter intuitive. This is just one example of why languages such as CoffeeScript have emerged.
All in all, I really enjoy programming in Javascript, and the more I understand it, the more enjoyable it becomes.
As Douglas Crockford put it, Javascript is the worlds most misunderstood language.