Javascript, lexical scopes and what your momma thought you about variables
Let us assume that we have this amazing javascript function:
function test() { var nums = [1,2,3,4,5,6,7]; for(var i = 0; i<nums.length; i++) { var alertLink = document.createElement("A"); alertLink.href = "#"; alertLink.innerHTML = nums[i]; if( nums[i] % 2 == 0) { alertLink.onclick = function() { alert('EVEN: '+ nums[i]); }; } else { alertLink.onclick = function() { alert('ODD: ' + nums[i]); }; } document.firstChild.appendChild(alertLink); document.firstChild.appendChild(document.createElement("BR")); } }
Can you guess what it would generate? Quite a few undefines alerts, as a matter of fact. Why is that? Because the anonymous function is a closure, which capture not the value of i, but the i variable itself.
This means when we click on a link that this method has generated, we use the last known value of i. Since we have exited the loop, i is actually 8.
Now, in C# we have the same problem, and we can solve it by introducing a temporary variable in the loop, so we change the code to look like this:
function test() { var nums = [1,2,3,4,5,6,7]; for(var i = 0; i<nums.length; i++) { var alertLink = document.createElement("A"); alertLink.href = "#"; alertLink.innerHTML = nums[i]; var tmpNum = nums[i]; if( nums[i] % 2 == 0) { alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); }; } else { alertLink.onclick = function() { alert('ODD: ' + tmpNum ); }; } document.firstChild.appendChild(alertLink); document.firstChild.appendChild(document.createElement("BR")); } }
Try to run it, and you'll get an.. interesting phenomenon. All the links will show tmpNum as 7. Again, we captured the variable itself, not its value. And in JS, it looks like you are getting the same variable in the loop, not a new one (this is absolutely the wrong way to describe it, but it is a good lie), like you would in C#.
What is even more interesting is that you would get the exact same result here:
function test() { var nums = [1,2,3,4,5,6,7]; for(var i = 0; i<nums.length; i++) { var alertLink = document.createElement("A"); alertLink.href = "#"; alertLink.innerHTML = nums[i]; if( nums[i] % 2 == 0) { var tmpNum = nums[i]; alertLink.onclick = function() { alert('EVEN: '+ tmpNum ); }; } else { var tmpNum = nums[i]; alertLink.onclick = function() { alert('ODD: ' + tmpNum ); }; } document.firstChild.appendChild(alertLink); document.firstChild.appendChild(document.createElement("BR")); } }
Here we have two different lexical scopes, with respectively different variables. Looks like it should work. But the lexical scope of JS is the function, not the nearest set of curly. Both tmpNum refer to the same variable, and as such, are keeping the last value in it.
If the lexical scope is a function, we need to use a function then. Here is a version that works:
function test() { var nums = [1,2,3,4,5,6,7]; for(var i = 0; i<nums.length; i++) { var alertLink = document.createElement("A"); alertLink.href = "#"; alertLink.innerHTML = nums[i]; if( nums[i] % 2 == 0) { var act = function(tmpEVEN) { alertLink.onclick = function() { alert('EVEN: '+tmpEVEN); }; }; act(nums[i]); } else { var tmpODD = nums[i]; alertLink.onclick = function() { alert('ODD: ' + tmpODD); }; } document.firstChild.appendChild(alertLink); document.firstChild.appendChild(document.createElement("BR")); } }
And that is it for today's JS lesson.
Comments
Good post, this stuff can get confusing at times. To be brutally honest when confronted with a situation like this, I often just hack it till it works (which, without casting aspersions, looks like maybe is what happened here? ;-).
In any event the feeling that I don't fully get how lexical scope works inside of closures certainly niggles. Need a better way to look at this problem.
PS You didn't 'fix' the tmpODD version. Is that just a typo?
Nice post, I have not completely get it in the head myself.
Nitpicking:
Why for so many people JS code is so much worse than their C# code?
JS allows much better DSLs and has great frameworks.
Personally, I would use something like map() on nums and, also, put the oddness in the link itself, which would allow me to reuse the whole onclick function.
I don't know why people say that C# is better than JS. Perhaps because they are familiar with it?
You do realize that this is code written to show certain behavior, right? Not a case of showing JS best practices
Sorry, I probably had a morning of bad English syntax today.
I know why people prefer C# -- for static typing. Large JS projects have scaling problems for large teams -- it is just too easy to break something somewhere (at least until someone does a good MbUnit/NMock2 port to JS. that is not so hard btw, but nobody did andd I have a trouble).
What I was talking about is that JS samples from people knowing .NET are generally of lower quality than their C# samples (do not know about Java or whatever).
Of course I understand that this is made to demonstrate the point. This is why I marked it as nitpicking). I just like that your C# samples are generally very creative and interesting even in the parts that do not demonstrate the point.
Yeah, that stumped me a few times too.
Morris Johns wrote a great article with interactive examples at:
http://blog.morrisjohns.com/javascript_closures_for_dummies
The "let" keyword in JS 2.0 (or maybe even 1.7, I don't keep them straight) allows one to have curly-based lexical scopes.
I've run into this before as well. Not sure why they decided to implement it this way, it seems to differ from every other language I've ever worked with. Well except for VB6 I guess, it didn't really have block level scoping, just declare the variable before you need it.
As far as Andrey's comment about Unit testing, with a port of RhinoMocks as well and a standalone JavaScript engine you could unit test all of Javascript outside of the browser. There's JSUnit, but alas you have to fire up a browser and run it. I don't think you can UnitTest Javascript that's embedded in your page either.
Another way to deal with this is to attach the value you want to the object that it is being used by. Something like this:
That way the scope of the variable is the same as the scope of the object using it.
Comment preview