Creating a closure in JavaScript

2011-11-24
I thought that I knew how closures worked in JavaScript, but then I was bitten by a bug that made me realize I had some more learning to do. This article is the result of that learning. It describes one particular situation where you need to explicitly create a closure  in JavaScript, but I also think it illustrates closures in general.

The problem

Take a very simple html page with three elements, like these:

<h1>A</h1>
<h1>B</h1>
<h1>C</h1>

Now assume you would like to bind the click event to all of the h1-elements on the page. To prove that something happens, I will just add a word to the contents of the h1 element when it is clicked. The code looks like this:

var elements = document.getElementsByTagName("h1");
for (var i = 0; i<elements.length; i++) {
	var el = elements[i];
	el.addEventListener("click", function() {
		el.innerHTML += ", clicked";
	}, false);
}

I thought this would work. Turns out it doesn’t. If you run it, all clicks will add the word to the last element. Even when you click the A, the word will be added to the end of the last element with a C.

You can try it out using jsFiddle here.

My mistake was assuming that a closure was made inside of my function doing the “el.innerHTML” assignment, making sure that the “el” variable was frozen it time and would always refer to the value/element I had when the code was executed. But that is not so. The rule here is that no matter where you declare a variable in JavaScript with the “var” keyword, it will always act as though it was declared at the beginning of the innermost function anyway.

As a side example, these two code sections are the same when it comes to the scope of the “str” variable.

function my() {
    for (var i = 0; i<10; i++) {
        var str = i.toString();
    }
}
function my() {
    var str;
    for (var i = 0; i<10; i++) {
        str = i.toString();
    }
}

The solution

The way to freeze the “el” variable at the time of calling addEventLister, is to explicitly create a closure. A closure is a JavaScript function. The function is the only container that will freeze a variable at the time of execution, which is what a closure is, really.

This is one way of ensuring that the code works as expected:

var elements = document.getElementsByTagName("h1");
for (var i = 0; i<elements.length; i++) {
    (function(el) {
        el.addEventListener("click", function() {
            el.innerHTML += ", clicked";
        }, false);
    })(elements[i]);
}

Notice how the call to addEventListener is now wrapped in an anonymous function, and how that anonymous function is also called in the same statement, by putting the “(elements[i])” at the end.

You can verify that the solution works at jsFiddle here.

If you like, you can also extract the closure part into a separate function, which may improve the readability of the code. One such alternative solutions would be:

function handleClick(el, func) {
    el.addEventListener("click", function(ev) { func(el); }, false);
}

var elements = document.getElementsByTagName("h1");
for (var i = 0; i<elements.length; i++) {
    handleClick(elements[i], function(el) {
        el.innerHTML += ", clicked";
    });
}

Or even slightly more generic and succinct:

function createHandler(el) { return function(ev) { el.innerHTML += ", clicked"; }; };
var elements = document.getElementsByTagName("h1");
for (var i = 0; i<elements.length; i++) {
	var el = elements[i];
	el.addEventListener("click", createHandler(el), false);
}

One difference in the last example is that the closure encloses less of the code (only the assignment of innerHTML). Also note that the function “createHandler” is return an anonymous function in itself. It could be called a “factory” since it doesn’t execute any code, but just returns created functions that will be called later.

I guess what solution you use is dependent on what you are trying to do.

 

Leave a Reply

Twitter: @mikeplate