1

Please refer the JSfiddle - http://jsfiddle.net/r0ejq8h8/

var buttons = document.getElementsByTagName("button");

for (i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function () {
    alert("You just clicked " + i);
});
}

It always alerts "You just clicked 5". I understand this. There might be many solutions for this problem. One of them I found is there in JSfiddle. No explanation in the net was good enough. Can someone please explain to me in detail as to what is happening with "i". Any other solution with explanation would also be great.

maze star
  • 29
  • 1
  • I'm on the fence about this being a duplicate so I'm not going to close it, but you can find some information on this here: http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – James Montagne Jan 05 '15 at 18:36

3 Answers3

1

It's easiest to think of this in terms of a timeline.

i is a variable name that points to a value. When you go through the loop (the first thing to happen), i is being constantly reset to a higher value (first 0, then 1 etc). Each time it iterates you bind a function to a button. That function is not called yet, and it references i.

Later, when a button is clicked, the function executes and looks for the value of i. Because the loop has completed at this point, i will be equal to the number of buttons (in this case 5).

The timeline

So basically:

  • i is set to 0

  • the first function is bound

  • i is set to 1

  • the second function is bound

...

  • i is set to n (the number of buttons)

  • i is greater than the loop condition and the loop completes

  • The user clicks a button

  • The callback is fired and references i which is currently set to n

  • the alert fires with n as its value.

Fixing the issue

Lots of ways to fix it. The easiest old (backwards compatible way) is to pass the current value to a function (so it is read right away), and then use that to return a new function which has saved the old value in a closure:

for (i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", createHandler(i));
}

function createHandler(val) {
  return function() {
    alert("You just clicked " + val);
  }
} 

the new (ES6) way that may not work in all browsers is to use let, which limits a variable to block scope.

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", function () {
    alert("You just clicked " + i);
  });
}
Ben McCormick
  • 25,260
  • 12
  • 52
  • 71
  • Pls see my fiddle, I have edited the solution. Have put "i" as a param for the inner function, any way to make it work? – maze star Jan 05 '15 at 18:38
  • @mazestar If you update a fiddle it creates a new URL (adding /1 or /2 etc.). The fiddle linked to in your question is unchanged. – James Montagne Jan 05 '15 at 18:41
  • @mazestar you were shadowing `i` by defining it within the inner function as well. Here it is fixed: http://jsfiddle.net/cjwwf219/ – Ben McCormick Jan 05 '15 at 18:56
0

When you click the button, you alert the value of i at the time the button is clicked.

This will be after the loop has finished and i is at its highest value.

This is because i is closed over by the anonymous function you pass to addEventListener.


By using another function, you copy the value of i at the time that that function is called.

var buttons = document.getElementsByTagName("button");

for (i = 0; i < buttons.length; i++) {
    add_event_handler(buttons[i], i);
}

function add_event_handler(element, value_to_alert) {
    element.addEventListener("click", function () {
        alert("You just clicked " + value_to_alert);
    });
  
}
<button>X</button>
<button>X</button>
<button>X</button>
<button>X</button>
Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
0

Taking your example,

var buttons = document.getElementsByTagName("button");

for (i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", function () {
    alert("You just clicked " + i);
  });
}

The anonymous functions you have registered for click event of the button encloses the value i. All are sharing same variable i and will have recent value of it which has been updated at the end of the for loop. And that is why the click event always alerts You just clicked 5


Fix

var buttons = document.getElementsByTagName("button");

function createAlert(i) {
    return function() {
        alert("You just clicked " + i);
    }
}

for (i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener("click",createAlert(i));
}

In the fix, the alert function now depends on the environment created by createAlert function. So for each iteration each of the returned function will enclose particular value of i given by the iteration of the for loop


Updated Fiddle

http://jsfiddle.net/r0ejq8h8/

Kelsadita
  • 1,038
  • 8
  • 21