0

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);
Community
  • 1
  • 1
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404

3 Answers3

2

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).

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • 1
    Oops, deleted my answer. I misread yours first, but it's correct. Might just be worth pointing out that the output the OP is seeing is immediate, while still in the `for` loop itself, and long before the 100ms delay. – jmar777 Oct 09 '14 at 18:03
  • Thanks @jmar777, I'm the OP :) I've included your note in the answer. Happy to delete both our comments if you'd like. – Dan Dascalescu Oct 10 '14 at 00:20
0

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.

Pointy
  • 405,095
  • 59
  • 585
  • 614
0

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • @DanDascalescu: Thanks, feel free to upvote and accept :-) In your answer you say something about strings however, which I'm not sure how it is relevant to the problem. – Bergi Oct 10 '14 at 04:37
  • I was linking to the [MDN docs on setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout), which explain that the first parameter can be either a function **or a string**, which is `eval()`ed. I believe `setTimeout()` converts the `undefined` returned by `console.log()` to en empty string, thus executing nothing after the timeout (or not even setting a timer). – Dan Dascalescu Oct 10 '14 at 06:02
  • Yes, that's what I would expect: If the argument is not a function (nor a string), then *nothing* is happening at all. – Bergi Oct 10 '14 at 06:32