Wednesday, October 11, 2006

Javascript Closures

Editors Note: I had this all edited and fixed up and then something funny happened and I lost it all. So you're getting the unedited version. I'm not doing it again and nobody reads this blog yet anyhow.

Javascript can be a frustrating language. At first it seems like BASIC with a C syntax. But quickly odd things start happening. They aren't really odd but they aren't what you expect. In order to do my part to save the world from odd things I present some thoughts on Javascript Closures. I recommend reading Javascript Closures by Richard Cornford.

Many of the odd things in Javascript begin to manifest themselves when you begin embedding functions inside other functions. This happens a lot when you dynamically assign event handlers to objects on a web page. These thoughts are mostly in that context.

Closures

Closures have to do with the way Javascript supports scoping in nested functions. A nested function has access to the scope of its parent functions. However, the scope of the parent functions can change after the nested function has been declared and before the nested function is actually called and executed.

It's common to assume that the nested function gets the parent scope when it is declared. But this is not the case. The parent scope is checked when the nested function is called. This also means the parent scope continues to exist after the parent function has ended.

Examples:
function parent() {
var parentVariable = "Parent Variable Before";

function child(childVariable) {
var childVariable2 = "Child Variable 2";
alert(parentVariable);
alert(childVariable);
alert(childVairable2);
}

child("Child Variable");

parentVariable = "Parent Variable After";

child("Child Vairable");
}


This example shows that the function child checks the parent function scope when it is called, and so alerts "Parent Variable After" at the second call. A common mistake is to assume that the child function will always alert "Parent Variable Before" since it was declared when parentVariable was set to that value.

This misconception causes trouble when trying to set onclick events in a for loop. The code usually looks something like this:

for (var i=1; i <= 10; i++) {
var new_element = document.createElement("div");
new_element.onclick = function (e) { alert(i); };
new_element.innerHTML = i;
document.appendChild(new_element);
}


In this example 10 divs are created. Each is numbered from 1 to 10. When you click each on you would expect the div to alert with its number. It does not. The alert displays 10 on all the divs.

This behavior makes sense if you think about the scope of the parent function and when the child (the onclick) function is being called versus when it is being declared. The child function does not use the parent scope when it is declared. It hasn't actually been executed yet. By the time it is called the parent scope (and the variable i has changed.

By the time you click a div and the onclick is called the i variable in the parent scope is set to 10 because the loop has finished and stopped at 10. If you add the line below after the for loop then each click on a div will display "Hello World". So, understanding the parent scope what did you expect? It's using the parent scope at the time of the click, not the scope when the onclick function was declared.

i = "Hello World";



Here is another example. You should be able to predict the behavior without running the script.

var my_function;
for (var i=1; i <= 10; i++) {
if (i == 5) {
my_function = function() { alert(i); };
}
}

my_function();


So, without running the script, will the call to my_function alert with a 5 or a 10? If you said 5 then shame on you. You'd better start again.

A Solution

It's not uncommon to want to assign event functions in a loop (like the onclick div example). But the obvious code example above doesn't do what you might expect (although it does do the right/correct thing). So, in order to make it work you need to either create a scope that's not going to change between the time you declare the inline function and the time you call the inline function or you could declare a variable that is not going to change in the parent scope. You can't use i because it's going to change each time you loop.

I use the the "create a scope" method. The only way I know to create a new scope in javascript is by calling in to a function.

(in Javascript loops do not have their own scope. Their variables are elevated to the existing scope so you can't use any kind of nested for. That is why i == 10 and not undefined in our div example above. Even though the i was declared in the for-loop, Javascript promotes it to the function's scope and it still exists after the loop has ended).

function SetOnClick(value) {
// A new scope is created when this function is called
return function(e) { alert(value); };
// Once the function exits value can never be changed again.
// So when this inline function is called we can be assured that value will still be the same
}


for (var i=1; i<=10; i++) {
var new_element = document.createElement("div");
new_element.onclick = SetOnClick(i); // Now this onclick gets the inline function returned by SetOnClick.
// Calling this function creates the new scope we need
new_element.innerHTML = i;
document.appendChild(new_element);
}

No comments:

Post a Comment