Dan Dascalescu
Dan Dascalescu

Reputation: 152095

Why does this for / setTimeout() code actually output the numbers from 0 to 9?

A common pitfall with JavaScript closures is running setTimeout() from a for loop, and expecting the counter to be passed with different values at each iteration, while in practice it gets assigned the last value before the setTimeout() functions execute:

for (i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i)
  }, 100);
}  // => prints "10" 10 times

One solution to this is to have an Immediately Invoked Function Expression:

for (i = 0; i < 10; i++)
  (function(j) {
    setTimeout(function foo() {
      console.log(j)
    }, 100);
  })(i);  // prints 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Another is to pass an extra callback argument to setTimeout() (which doesn't work in IE<9):

for (i = 0; i < 10; i++) {
  setTimeout(function foo(n) {
    console.log(n)
  }, 100, i);
}

But why does the following, simplest, code, produce the same result (0, 1, 2, ... 9)?

for (var i = 0; i < 10; i++)
  setTimeout(console.log(i), 100);

Upvotes: 0

Views: 847

Answers (3)

Bergi
Bergi

Reputation: 664599

why does the following, simplest, code, produce the same result (0, 1, 2, ... 9)?

for (var i = 0; i < 10; i++)
  setTimeout(console.log(i), 100);

Because it actually doesn't. If you look closely, you will notice that the log messages will not need the tenth of a second before they appear in console. By calling the console.log(i) right away, you are only passing the result of the call (undefined) to setTimeout, which will do nothing later. In fact, the code is equivalent to

for (var i = 0; i < 10; i++) {
  console.log(i);
  setTimeout(undefined, 100);
}

You will notice the difference better if you replace the 100 by i*500 in all your snippets, so that the log messages should be delayed at an interval of a half second.

Upvotes: 0

Dan Dascalescu
Dan Dascalescu

Reputation: 152095

This apparently surprising behavior occurs because the first parameter to setTimeout can be a function as well as a string, the latter being eval()-ed as code.

So setTimeout(console.log(i), 100); will execute console.log(i) right away, which returns undefined. Then setTimeout("", 100) will be executed, with a NOP call after 100ms (or optimized away by the engine).

Upvotes: 2

Pointy
Pointy

Reputation: 413737

Just for grins, another thing you can do (when you've got .bind()) is

for (i = 0; i < 10; i++) {
  setTimeout(function () {
    var i = +this;
    console.log(i)
  }.bind(i), 100);
}

A little bit less of a mess than an IIFE.

Upvotes: 0

Related Questions