0

I'm learning some ES6 features and of course came across the let keyword and its new scope (differs from var) and I came across an example about the tricky scope of var and its hoisting. but I can't fully understand why I get this result:

var events = ['click', 'dblclick', 'keydown', 'keyup'];

for (var i = 0; i < events.length; i++) {
  var event = events[i];
  document.getElementById('btn').addEventListener(event, function() {
    document.getElementById('result').innerHTML = 'event: ' + event;
  });
}
<button id="btn">Click Me!</button>
<span id="result"></span>

I understand that var event is hoisted outside the for loop but why is it getting the last event ('keyup') in the array every iteration of the loop?
Is the addEventListener function asynchronous and by the time its attaching the event the value of the event changes?

Sagiv b.g
  • 30,379
  • 9
  • 68
  • 99
  • this is not so much about hoisting as it is about variable scope and closures. – Sirko Jan 01 '16 at 08:57
  • 1
    *I understand that the element ('btn') is hoisted outside the for loop.* Elements are not hoisted; variables are. –  Jan 01 '16 at 13:09
  • @torazaburo thanks, my mistake. fixed it. – Sagiv b.g Jan 01 '16 at 13:17
  • Just replace the line `var event = events[i];` with `let event = events[i];`. Meanwhile you could de-accept the answer which proposes the clumsy use of anonymous functions, no longer necessary with block-scope variables. –  Jan 01 '16 at 13:18
  • @torazaburo i think you did not understand my question at all. i know about `let` and it's block scoping and about `var` and it's function scoping. my problem was that i didn't understand that there is only 1 instance of the same variable and not 4 different instances. @void helped me understand that, hence his answer is valid. i know you can solve this problem in other ways beside an IIFE (like `.bind`) but that's irrelevant to my question. – Sagiv b.g Jan 01 '16 at 13:22
  • I think I understood your question perfectly. It you know about `let` then why didn't you use it? The whole point is that there **are** four different "instances" if you use `let`. Yes, there are different approaches, but some are old and clumsy and unnecessary, and others are sleek and modern--like using `let`. That was what it was designed for! –  Jan 01 '16 at 13:28
  • but the question wasnt "what is best practice as of 2016 regarding Variables Scope..." i wanted to understand why the weakness of `var` with function scope make this **specific** code "act" like it did. – Sagiv b.g Jan 01 '16 at 13:30

2 Answers2

2

In your example all the events are being registered, but the inner event variable and the outer event variables are different as there is no block level scope and but function level scope.

These 4 events 'click', 'dblclick', 'keydown', 'keyup' are all registered but as the value of event at the end becamse keyup so 'event: ' + event; will always be event: keyup.

You can use IIFE ( immediately-invoked function expression ), it is a JavaScript design pattern which produces a lexical scope using JavaScript's function scoping.

var events = ['click', 'dblclick', 'keydown', 'keyup'];

for (var i = 0; i < events.length; i++) {
  var event = events[i];
  (function(event) {
    document.getElementById('btn').addEventListener(event, function() {
      document.getElementById('result').innerHTML = 'event: ' + event;
    });
  })(event);
}
<button id="btn">Click Me!</button>
<span id="result"></span>

Trying to explain:

Try clicking it, ev is keyup and see the behaviour after 4 seconds.

var events = ['click', 'dblclick', 'keydown', 'keyup'];

for (var i = 0; i < events.length; i++) {
  var event = events[i];
  var ev = events[i];
  document.getElementById('btn').addEventListener(event, function() {
    document.getElementById('result').innerHTML = 'event: ' + ev;
  });
  
  setTimeout(function(){
    ev = "Look it is changed now, see the scoping!";
  },4000);
}
<button id="btn">Click Me!</button>
<span id="result"></span>
void
  • 36,090
  • 8
  • 62
  • 107
  • i know that i can bypass this behavior with an IIFE but i still can't understand why is it the last value and not lets say the first value of the array ('click') – Sagiv b.g Jan 01 '16 at 09:11
  • @Sag1v see the updated answer. If it solves please mark it as the right answer. – void Jan 01 '16 at 09:15
  • i think i got it now. so basically it creates 1 spot in the memory instead of 4 different spots? – Sagiv b.g Jan 01 '16 at 09:29
  • Yes you can say that. – void Jan 01 '16 at 09:29
  • great thanks for clearing this for me :) – Sagiv b.g Jan 01 '16 at 09:31
  • Since the OP went to the trouble of mentioning `let` in his question, why do you not propose the trivial solution of `let event = events[i];`, which eliminates the need for the anonymous function? –  Jan 01 '16 at 13:16
  • @torazaburo because that was not my question. see my comment for you above – Sagiv b.g Jan 01 '16 at 13:27
1

There is indeed something "asynchronous" (sort of) happening, as the two event occurrences are not evaluated at the same time.

document.getElementById('btn').addEventListener(event, function() {
    document.getElementById('result').innerHTML = 'event: ' + event;
  });

The first event is evaluated right away in the loop, so get the 4 values.

The second eventid evaluated when the anonymous function containing it is executed, i.e. when the event fires. At that moment, the JS engine goes back to the event var that it has somehow kept a reference to (that's a closure), and finds the last value assigned to it.

Ilya
  • 5,377
  • 2
  • 18
  • 33
  • This has nothing to do with asynchronicity. –  Jan 01 '16 at 13:13
  • That's why I've put quotes around "asynchronous". The main point, and what is (probably) surprising the OP, is that the evaluation of event inside the function is not immediate but deferred to the execution of the function. It just so happens that in this particular case that execution is also really asynchronous (triggered by an event). – Ilya Jan 01 '16 at 14:32