3

Trying to grasp the differences between the two and I am experimenting with different examples to ensure I understand properly.

I have already looked at this question on Stack Overflow: What's the difference between using "let" and "var" to declare a variable?

It seemed to make sense until I stumbled across this code segment: http://jsbin.com/xepovisesi/edit?html,output

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

  for(var i = 0; i < buttons.length; i++) {
      const button = buttons[i];
      button.addEventListener("click", function() {
          alert("Button " + i + " Pressed");
      });
  }
<button />
<button />
<button />
<button />

If you change the first for loop statement to a let, the functionality works as expected... It alerts which button was pressed. However var just alerts "Button 10 was pressed" every time. Why is it doing this?

Thanks, James.

Community
  • 1
  • 1
James Barrett
  • 2,757
  • 4
  • 25
  • 35

5 Answers5

4

It's explained in the first paragraph of the accepted answer for the linked question:

The difference is scoping. var is scoped to the nearest function block and let is scoped to the nearest enclosing block (both are global if outside any block), which can be smaller than a function block.

var creates a variable in the scope outside for statement block. The equivalent code with let looks like this:

  let i = 0;
  for(i = 0; i < buttons.length; i++) {
      const button = buttons[i];
      button.addEventListener("click", function() {
          alert("Button " + i + " Pressed");
      });
  }

Event listener for every button is bound to a single instance of i, which has value 10 after for loop ends, and you see that when you press each button.

This code

  for(let i = 0; i < buttons.length; i++) {
      const button = buttons[i];
      button.addEventListener("click", function() {
          alert("Button " + i + " Pressed");
      });
  }

has i in the scope of the of the for statement block. That means that for each iteration, new instance of i is created and bound in the event handler, so each handler shows different value for i.

Without let you can create a scope by introducing a function. The common way to write such code before let was

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

  for(var i = 0; i < buttons.length; i++) {
      addButtonListener(i);
  }
  function addButtonListener(i) {
      const button = buttons[i];
      button.addEventListener("click", function() {
          alert("Button " + i + " Pressed");
      });
  }
artem
  • 46,476
  • 8
  • 74
  • 78
3

This is not indeed obvious and needs to be specified.

The reason is that let in a for(...) statement creates a new "binding" at each iteration. It's this way because the standard says so, but it could have been different without changing the meaning of let in other cases.

For example in Common Lisp it is unspecified (is implementation dependent) if a (dotimes ...) form will create a new binding at each iteration or not.

In other words Javascript code like:

for (let i=0; i<10; i++) {
   ...
}

is equivalent to

{
    let __hidden__ = 0;
    for (__hidden__ = 0; __hidden__ < 10; __hidden__++) {
        let i = __hidden__;
        ...
    }
}

and it's NOT the same as

{
    let i = 0;
    for (i = 0; i<10; i++) {
        ...
    }
}

I personally think that this is indeed the most useful semantic (a new binding at each iteration) as reusing the same binding can be surprising in case of captures. It's also much easier to implement the other semantic when you need it (the second snipped is simpler and shorter than the first).

Reusing the same binding is instead for example what Python 3 does for variables in comprehensions:

[(lambda : i) for i in range(10)][3]() # ==> 9

Finally in my opinion the worst possible choice is leaving it implementation-dependent ... what Common Lisp surprisingly did...

(let ((L (list)))
  (dotimes (i 10)
    (push (lambda () i) L))
  ;; May be the following will print 10 times 10, or may
  ;; be the numbers from 9 to 0
  (dolist (f L)
    (print (funcall f))))
6502
  • 112,025
  • 15
  • 165
  • 265
2

The difference is scope. In this example, you are creating a javascript closure (By having the event listener inside the for loop). Javascript does not write the value into the lexical scope of the function at the time the function is defined, rather, it looks up the value when the function is executed. When you use var, it will define the variable i inside the scope of whatever function it is inside of. In this case, the global scope. So each loop iteration refers to the same i variable, which gets incremented to 10 rather instantaneously, and the variable lives on past the for loop. So, when the event listener fires, it references the i variable on the global scope, whose value remains 10.

On the other hand, using let creates a LOCAL copy of i, which is scoped to its respective loop iteration. When the event listener is fired, it checks its own scope for the variable i which it obviously doesn't find, and then it moves outward, with the next step being the scope of the for loop to which it finds the local copy of i which retained its value for that specific iteration of the loop - hence you get the correct alert message for each button that is pressed.

Summary / TLDR;

  • var scopes variables to the function in which it is defined

  • let scopes variables to the block in which it is defined

  • Variables referenced in closures work their way outward through the scope chain when searching for a definition of a variable, stopping at the global scope.

Hope this helps!

Community
  • 1
  • 1
mhodges
  • 10,938
  • 2
  • 28
  • 46
0

Variable 'i' is being captured inside your button event handler.

1) If you don't have 'let' available, you can resolve this by scoping a new variable inside a IIFE like below:

 const button = buttons[i];
(function() {
  var j = i;
  button.addEventListener("click", function() {
  alert("Button " + j + " Pressed");
  });
})()

2) Or, if you have 'let' available just do:

 const button = buttons[i];
 let j = i;
  button.addEventListener("click", function() {
      alert("Button " + j + " Pressed");
  });
Klinger
  • 4,900
  • 1
  • 30
  • 35
  • That doesn't really answer the OP's question. – Felix Kling Dec 01 '16 at 05:12
  • I responded to the question in the body of the post: "If you change the first for loop statement to a let, the functionality works as expected... It alerts which button was pressed. However var just alerts "Button 10 was pressed" every time. Why is it doing this? " – Klinger Dec 01 '16 at 05:28
  • And where exactly are you explaining *why* `var` behaves the way it does, and what it is that happens here? – Felix Kling Dec 01 '16 at 05:38
0

The reason for this is because the var keyword is hoisted to the top of scope, where as let is block scoped (closest curly brace). Let me explain by showing you how code is pseudo-interpreted by the JavaScript runtime.

Code using var...

function () {
  for(var i = 0; i < buttons.length; i++) {
      var button = buttons[i];
      [...]
  }
}

...gets converted to something like this by the JS runtime (due to hoisting):

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

The thing to notice here is that the variables only exist once. These variables get updated on each iteration of the for loop. When the for loop is finished, all of the variables are holding the "last" value from the very last iteration.

Whereas the same code code using let...

function () {
  for(let i = 0; i < buttons.length; i++) {
      let button = buttons[i];
      [...]
  }
}

...gets treated differently. The variables are scoped to the curly braces around the for loop. Conceptually, you can think of it like a forEach statement:

function () {
  buttons.forEach(function(item, i) {
     var button = buttons[i]; // same as "item"
     var length = buttons.length;
     [...]
  });
}

The thing to notice is that every iteration of the for loop gets its own variable i, length, and button - no sharing taking place. This is the distinction.

Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96