Javascript multiple dynamic addEventListener created in for loop - passing parameters not working

I want to use event listeners to prevent event bubbling on a div inside a div with onclick functions. This works, passing parameters how I intended:

<div onclick="doMouseClick(0, 'Dog', 'Cat');" id="button_id_0"></div>
<div onclick="doMouseClick(1, 'Dog', 'Cat');" id="button_id_1"></div>
<div onclick="doMouseClick(2, 'Dog', 'Cat');" id="button_id_2"></div>

<script>
function doMouseClick(peram1, peram2, peram3){
    alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}
</script>

However, I tried to create multiple event listeners in a loop with this:

<div id="button_id_0"></div>
<div id="button_id_1"></div>
<div id="button_id_2"></div>

<script>
function doMouseClick(peram1, peram2, peram3){
    alert("doMouseClick() called AND peram1 = "+peram1+" AND peram2 = "+peram2+" AND peram3 = "+peram3);
}

var names = ['button_id_0', 'button_id_1', 'button_id_2'];

    for (var i=0; i<names.length; i++){

        document.getElementById(names[i]).addEventListener("click", function(){
        doMouseClick(i, "Dog", "Cat");

    },false);

}

</script>

It correctly assigns the click function to each div, but the first parameter for each, peram1, is 3. I was expecting 3 different event handlers all passing different values of i for peram1.

Why is this happening? Are the event handlers not all separate?


Solution 1:

Problem is closures, since JS doesn't have block scope (only function scope) i is not what you think because the event function creates another scope so by the time you use i it's already the latest value from the for loop. You need to keep the value of i.

Using an IIFE:

for (var i=0; i<names.length; i++) {
  (function(i) {
    // use i here
  }(i));
}

Using forEach:

names.forEach(function( v,i ) {
  // i can be used anywhere in this scope
});

Solution 2:

2022 edit

As someone is still reading and upvoting this answer 9 years later, here is the modern way of doing it:

for (const [i, name] of names.entries()) {
    document.getElementById(name).addEventListener("click", () => doMouseClick(i, "Dog", "Cat"), false);
}

Using const or let to define the variables gives them block-level scope and the value of i passed to the handler function is different for each iteration of the loop, as intended.

The old ways will still work but are no longer needed.

2013 answer

As pointed out already the problem is to do with closures and variable scope. One way to make sure the right value gets passed is to write another function that returns the desired function, holding the variables within the right scope. jsfiddle

var names = ['button_id_0', 'button_id_1', 'button_id_2'];

function getClickFunction(a, b, c) {
  return function () {
    doMouseClick(a, b, c)
  }
}
for (var i = 0; i < names.length; i++) {
  document.getElementById(names[i]).addEventListener("click", getClickFunction(i, "Dog", "Cat"), false);
}

And to illustrate one way you could do this with an object instead:

var names = ['button_id_0', 'button_id_1', 'button_id_2'];

function Button(id, number) {
  var self = this;
  this.number = number;
  this.element = document.getElementById(id);
  this.click = function() {
    alert('My number is ' + self.number);
  }
  this.element.addEventListener('click', this.click, false);
}
for (var i = 0; i < names.length; i++) {
  new Button(names[i], i);
}

or slightly differently:

function Button(id, number) {
  var element = document.getElementById(id);
  function click() {
    alert('My number is ' + number);
  }
  element.addEventListener('click', click, false);
}
for (var i = 0; i < names.length; i++) {
  new Button(names[i], i);
}

Solution 3:

It's because of closures.

Check this out: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Creating_closures_in_loops_A_common_mistake

The sample code and your code is essentially the same, it's a common mistake for those don't know "closure".

To put it simple, when your create a handler function, it does not just accesses the variable i from the outer environment, but it also "remembers" i.

So when the handler is called, it will use the i but the variable i is now, after the for-loop, 2.