Brett's Blog
Edit     New

Wednesday, December 17, 2008

Although this is covered in many JavaScript books, such as the excellent JavaScript: The Good Parts, I find the following issue with the potentially very powerful nested functions (or closures) to be especially under-explained in the literature.

The following is sample code which adds a method to three different objects, expecting that the alert will indicate a different value on each object.

function add (objs) {
for (var i=0; i < objs.length; i++) {
objs[i].showIValue = function () {
alert(i);
}
}
}

var a = {};
var b = {};
var c = {};
var objs = [a, b, c];
add(objs);

a.showIValue(); // Gives '3'
b.showIValue(); // Also gives '3'
c.showIValue(); // And, yet again, '3'


So, what's going on here? Nested functions inherit the scope of those functions in which they are nested, with the variables in that scope treated as the variables themselves--not as mere copies of the variables. And the variable is considered for its value by the end of the function call, in this case, the last increment of 'i', which was equal to the length of the passed-in array.

You can confirm this strangeness if you add a line at the end of the function to produce this:

function add (objs) {
for (var i=0; i < objs.length; i++) {
objs[i].showIValue = function () {
alert(i);
}
}
i += 100;
}


Now each of your method calls will give '103', even though the increment is at the end of the function! (This is therefore a different case from one you may have heard of in which the function uses define-time values--more on that below.)

If you change the function to have 'i' become a global variable, and then tamper with the 'i' variable after the add() method is called, you can also see how the variable is considered for its call-time value.


var i;
function add (objs) {
for (i=0; i < objs.length; i++) {
objs[i].showIValue = function () {
alert(i);
}
}
}

var a = {};
var b = {};
var c = {};
var objs = [a, b, c];
add(objs);
i += 500;
a.showIValue(); // Gives '503'
i += 500;
b.showIValue(); // Gives '1003'
i += 500;
c.showIValue(); // Gives '1503'


I think the easiest way to summarize this might be to say that the variable is considered for its latest outer call-time value, bearing in mind that for local variables, a kind of memory is retained of their latest state at call-time--i.e., their value at the end of the function (since if we considered it as a global, 'i' would be undefined since it has no value in our global scope).

So, what's the solution?

We can do:

function add (objs) {
for (var i=0; i < objs.length; i++) {
(function (i) {
objs[i].showIValue = function () {
alert(i);
}
})(i);
}
}


or

function add (objs) {
for (var i=0; i < objs.length; i++) {
objs[i].showIValue = function (i) {
return function () {
alert(i);
}
}(i);
}
}




In either case, we get the following results:

var a = {};
var b = {};
var c = {};
var objs = [a, b, c];
add(objs);

a.showIValue(); // Gives '0'
b.showIValue(); // Gives '1'
c.showIValue(); // Gives '2'


Note: I added the color highlighting for the above using the information here


Google
 
Brett's Blog Web