1

The title seems like a piece of cake, but that is not exactly what I am trying to ask. I just can't think of a better title, if you have any after reading the following, please comment and I will update the title. With jQuery, it is quite easy to do this, because click() passes this parameter,

$('img').click(function(){console.log($(this).attr('id')})

I was trying to do this using vanilla javascript, and encountered some pitfall

imgs = document.getElementsByTagName('img');
l = imgs.length;
for (i=0;i<l;i++) {
  imgs[i].addEventListener('click',function(){console.log(imgs[i].id)});
}

This won't work because every listner will be trying to console.log(imgs[l].id).

I have worked out a workaround:

imgs = document.getElementsByTagName('img');
l = imgs.length;
listener_generator = function(id){
  var s = function(){
   console.log(id);
  };
  return s;
};
for (i=0;i<l;i++) {
  imgs[i].addEventListener('click',listener_generator(imgs[i].id));
}

For learning purpose, I kind of remember when I read some javascript book, besides the approach above, there are other ways to do this, but I can't find it now. My question is, is there any other way to do this? And which one is the best practice?

Zakaria Acharki
  • 66,747
  • 15
  • 75
  • 101
shenkwen
  • 3,536
  • 5
  • 45
  • 85
  • Possible duplicate of [JavaScript closure inside loops – simple practical example](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Ziv Weissman Feb 24 '16 at 18:34

2 Answers2

1

This is a classic closure problem, which you stumbled into solving perhaps unintentionally.

The problem with this code:

for (i=0;i<l;i++) {
    imgs[i].addEventListener('click',function(){console.log(imgs[i].id)});
}

is basically that console.log(imgs[i].id) is not evaluated immediately, it is evaluated when the event listener is triggered. What this means is that you have lots of expressions that refer to i, but since they don't execute until the event listener fires, they all hold the exact same value of i (the last value it had at the end of the for loop).

To get around this, you need to "close over" the value of i to sort of capture its value in the for loop instead of later on when the event listener fires. There are multiple ways to do this - my preferred method is to create an anonymous function that executes for every iteration of the for loop, like this:

for (i=0;i<l;i++) {
    (function(_i){
        imgs[_i].addEventListener('click',function(){console.log(imgs[_i].id)});
    })(i);
}

You've basically accidentally solved this by creating a closure, only you're doing it with a named function instead of an anonymous one, but the result is the same. At runtime, the for loop calls a function which "captures" the value of i in a closure instead of deferring it until the event listener fires.

Here is an excellent post with more detail about how closures work.

Community
  • 1
  • 1
jered
  • 11,220
  • 2
  • 23
  • 34
1

The first code should work just if you're replacing img[i] by this inside click event to refer to the current img :

imgs = document.getElementsByTagName('img');
l = imgs.length;

for (i=0;i<l;i++) {
    imgs[i].addEventListener('click',function(){console.log(imgs[i].id)});
}

Hope this helps.


imgs = document.getElementsByTagName('img');

l = imgs.length;

for (i=0;i<l;i++) {
  imgs[i].addEventListener('click',function(){alert(this.id)});
}
<img src='http://tny.im/3Qp' id='img_1' />
<img src='http://tny.im/3Qp' id='img_2' />
<img src='http://tny.im/3Qp' id='img_3' />
<img src='http://tny.im/3Qp' id='img_4' />
Zakaria Acharki
  • 66,747
  • 15
  • 75
  • 101