2

I was watching Javascript Essentials by Travis Tidwell where he explained this piece of code:

(function() {
    var messages = ['hello', 'there'];

    for (var i in messages) {
        setTimeout(function() {
            console.log(messages[i]);
        }, 10); 
    };
})();

It echoes 'there' twice in the console, but I still don't understand exactly why. Could somebody go through this piece of javascript with me step by step?

Stephan-v
  • 19,255
  • 31
  • 115
  • 201
  • 3
    See: http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – gen_Eric Sep 23 '15 at 14:40
  • This is the infamous `closure inside loop` problem. You will find tons of information if you search for this. Essentially the reason for this behavior is that the for loop has already finished by the time the timeouts fire. At that point of time `i` has the value of the last loop, for both timeouts. – devnull69 Sep 23 '15 at 14:41

2 Answers2

5

Each time the code goes around the loop it sets an event handler so that after 10ms have passed, it logs the value of messages[i].

Before 10ms have passed for any of those timeouts, the value of i has been changed (by the for loop) to 1 (because that is the last property name in the array).

The first timeout then outputs messages[1], then the second timeout outputs messages[1].


  1. Array is created and stored in messages
  2. i is set to 0 and a timeout is set
  3. i is set to 1 and a timeout is set
  4. First timed out function runs, i is still 1
  5. Second timed out function runs, i is still 1
Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
  • Why is the behavior the same if you replace "10" with "0"? – johnnyRose Sep 23 '15 at 14:41
  • 3
    @johnnyRose browsers impose a minimum timeout value that is usually more than 10 milliseconds. – Pointy Sep 23 '15 at 14:42
  • @johnnyRose: Because `setTimeout(..., 0)` doesn't mean "run right away". It will basically push it to the bottom of the run queue and run it *after* the loop finishes. – gen_Eric Sep 23 '15 at 14:42
  • 3
    Because (a) There is a minimum timeout enforced and (b) JavaScript is single threaded, the event loop doesn't look for events to handle while it is in the middle of running another function. – Quentin Sep 23 '15 at 14:43
  • 1
    @johnnyRose As a proof of concept to show what the other commenters have said, see [this fiddle](http://jsfiddle.net/7p4goygq/1/). Open the console and see that there are two _"loop iteration"_ messages before either _"there"_ message... – War10ck Sep 23 '15 at 14:45
  • Okay so I think I got it. But it is really strange that when the timeout is set it doesn't store the i[0] count on the first loop. Instead it pretty much ignores whatever is within the function till after the 10 ms. – Stephan-v Sep 23 '15 at 14:46
  • 2
    "But it is really strange that when the timeout is set it doesn't store the i[0] count" — No, it isn't. That's a fundamental feature of JavaScript. Creating a function doesn't immediately create a locally scoped copy of every variable and freeze their values. The function doesn't even look at `i` until it comes time to use it. – Quentin Sep 23 '15 at 14:49
  • Atleast I am starting to get the hang of it. Thanks for taking your time! – Stephan-v Sep 23 '15 at 14:50
3

JavaScript has function scope, and not block scope like other languages. So, there exists in fact only a single i variable. By the time the code inside setTimeout is called, i is already set to the last index of the array.

Soon, in ECMAScript 6, we can declare block-scoped variables with let. See here: Mozilla Reference: let

Until then, one of the ways around this behavior is to create a new function for the variable that needs to be independent of the others:

(function() {
  var messages = ['hello', 'there'];

  for (var i in messages) {
    (function(currentIndex) {
      setTimeout(function() {
        logToOutput(messages[currentIndex]);
      }, 10);
    })(i);
  };
})();

function logToOutput(msg) {
  document.getElementById("output").innerHTML += msg + "<br>";
}
<div id="output"></div>
Heitor Chang
  • 6,038
  • 2
  • 45
  • 65