views:

53

answers:

3

I'm relatively new to JS so this may be a common problem, but I noticed something strange when dealing with for loops and the onclick function. I was able to replicate the problem with this code:

<html>
<head>

<script type="text/javascript">
window.onload = function () {
    var buttons = document.getElementsByTagName('a');
    for (var i=0; i<2; i++) {
        buttons[i].onclick = function () {
            alert(i);
            return false;
        }
    }
}

</script>

</head>

<body>
<a href="">hi</a>
<br />
<a href="">bye</a>

</body>

</html>

When clicking the links I would expect to get '0' and '1', but instead I get '2' for both of them. Why is this?

BTW, I managed to solve my particular problem by using the 'this' keyword, but I'm still curious as to what is behind this behavior.

A: 

It's a order of execution issue

http://stackoverflow.com/questions/1104321/how-to-assign-event-callbacks-iterating-an-array-in-javascript-jquery/1104440#1104440

Basically, the click handler accesses i well after the loop has exited, and therefore i is equal to 2.

Peter Bailey
It is also a scope issue, `i` is hanging around when it is not apparent it should.
Greg
Right, which is why I linked to a post of mine about closures.
Peter Bailey
+5  A: 

You need to store the state of the i variable, because by the time the event fires, the scoped state of i has increased to the maximum loop count.

window.onload = function () {
    var buttons = document.getElementsByTagName('a');
    for (var i=0; i<2; i++) {
        (function (i) {
            buttons[i].onclick = function () {
                alert(i);
                return false;
            }
        })(i);
    }
}

The above example creates an anonymous function with a single argument i, which is then called with i being passed as that argument. This creates a new variable in a separate scope, saving the value as it was at the time of that particular iteration.

Andy E
+2  A: 

You are having a very common closure problem in the for loop.

Variables enclosed in a closure share the same single environment, so by the time the onclick callback is executed, the loop has run its course and the i variable will be left pointing to the last entry.

You can solve this with even more closures, using a function factory:

function makeOnClickCallback(i) {  
   return function() {  
      alert(i);
      return false;
   };  
} 

var i;

for (i = 0; i < 2; i++) {
    buttons[i].onclick = makeOnClickCallback(i);
}

This can be quite a tricky topic, if you are not familiar with how closures work. You may to check out the following Mozilla article for a brief introduction:


Note: I would also suggest not to use var inside the for loop, because this may trick you in believing that the i variable has block scope, when on the other hand the i variable is just like the buttons variable, scoped within the function.

Daniel Vassallo