83

This code logs 6, 6 times:

(function timer() {
  for (var i=0; i<=5; i++) {
    setTimeout(function clog() {console.log(i)}, i*1000);
  }
})();

But this code...

(function timer() {
  for (let i=0; i<=5; i++) {
    setTimeout(function clog() {console.log(i)}, i*1000);
  }
})();

... logs the following result:

0
1
2
3
4
5

Why?

Is it because let binds to the inner scope each item differently and var keeps the latest value of i?

Badacadabra
  • 8,043
  • 7
  • 28
  • 49
user2290820
  • 2,709
  • 5
  • 34
  • 62
  • 3
    See here - http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword – eithed Jul 08 '15 at 07:22
  • It's worth noting that there are difference between the old Mozilla implementation of `let` and the new ES2015 version. However, for the specifics of this question, the dupe answers it just fine. – Andy E Jul 08 '15 at 08:11
  • 10
    Not a duplicate in my opinion. Every time somebody askes about let or var, we cant point to them to a very general answer. This is specifically asking about setTimeout(), which creates a 'closure within a loop' - a common var hoisting problem scenario - the answer and example below is not detailed in the linked duplicate accepted answer – Drenai Dec 10 '16 at 15:50
  • Closely related: [JavaScript closure inside loops – simple practical example](https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Ivar Aug 21 '19 at 13:51

2 Answers2

87

With var you have a function scope, and only one shared binding for all of your loop iterations - i.e. the i in every setTimeout callback means the same variable that finally is equal to 6 after the loop iteration ends.

With let you have a block scope and when used in the for loop you get a new binding for each iteration - i.e. the i in every setTimeout callback means a different variable, each of which has a different value: the first one is 0, the next one is 1 etc.

So this:

(function timer() {
  for (let i = 0; i <= 5; i++) {
    setTimeout(function clog() { console.log(i); }, i * 1000);
  }
})();

is equivalent to this using only var:

(function timer() {
  for (var j = 0; j <= 5; j++) {
    (function () {
      var i = j;
      setTimeout(function clog() { console.log(i); }, i * 1000);
    }());
  }
})();

using immediately invoked function expression to use function scope in a similar way as the block scope works in the example with let.

It could be written shorter without using the j name, but perhaps it would not be as clear:

(function timer() {
  for (var i = 0; i <= 5; i++) {
    (function (i) {
      setTimeout(function clog() { console.log(i); }, i * 1000);
    }(i));
  }
})();

And even shorter with arrow functions:

(() => {
  for (var i = 0; i <= 5; i++) {
    (i => setTimeout(() => console.log(i), i * 1000))(i);
  }
})();

(But if you can use arrow functions, there's no reason to use var.)

This is how Babel.js translates your example with let to run in environments where let is not available:

"use strict";

(function timer() {
  var _loop = function (i) {
    setTimeout(function clog() {
      console.log(i);
    }, i * 1000);
  };

  for (var i = 0; i <= 5; i++) {
    _loop(i);
  }
})();

Thanks to Michael Geary for posting the link to Babel.js in the comments. See the link in the comment for a live demo where you can change anything in the code and watch the translation taking place immediately. It's interesting to see how other ES6 features get translated as well.

rsp
  • 107,747
  • 29
  • 201
  • 177
  • 4
    Just to add to your excellent explanation, here is a [translation of the ES6 code to ES5 provided by babeljs.io](http://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=(function%20timer()%20%7B%0D%0A%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%3D%205%3B%20i%2B%2B)%20%7B%0D%0A%20%20%20%20setTimeout(function%20clog()%20%7B%20console.log(i)%3B%20%7D%2C%20i%20*%201000)%3B%0D%0A%20%20%7D%0D%0A%7D)()%3B). – Michael Geary Jul 08 '15 at 07:40
  • @MichaelGeary Thanks for the link. I added the Babel translation to my answer. Thanks. – rsp Jul 08 '15 at 07:50
  • 1
    Scoping is not relevant here, it's down to the new binding created at each iteration – seriousdev Apr 18 '17 at 20:00
11

Technically it's how @rsp explains in his excellent answer. This is how I like to understand things work under the hood. For the first block of code using var

(function timer() {
  for (var i=0; i<=5; i++) {
    setTimeout(function clog() {console.log(i)}, i*1000);
  }
})();

You can imagine the compiler goes like this inside the for loop

 setTimeout(function clog() {console.log(i)}, i*1000); // first iteration, remember to call clog with value i after 1 sec
 setTimeout(function clog() {console.log(i)}, i*1000); // second iteration, remember to call clog with value i after 2 sec
setTimeout(function clog() {console.log(i)}, i*1000); // third iteration, remember to call clog with value i after 3 sec

and so on

since i is declared using var, when clog is called, the compiler finds the variable i in the nearest function block which is timer and since we have already reached the end of the for loop, i holds the value 6, and execute clog. That explains 6 being logged six times.

Quannt
  • 2,035
  • 2
  • 21
  • 29