66

The following alerts 2 every time.

function timer() {
    for (var i = 0; i < 3; ++i) {
        var j = i;
        setTimeout(function () {
            alert(j);
        }, 1000);
    }
}

timer();

Shouldn't var j = i; set the j into the individual scope of the setTimeout?

Whereas if I do this:

function timer() {
    for (var i = 0; i < 3; ++i) {
        (function (j) {
            setTimeout(function () {
                alert(j);
            }, 1000);
        })(i);
    }
}

timer();

It alerts 0, 1, 2 like it should.

Is there something I am missing?

Naftali
  • 144,921
  • 39
  • 244
  • 303
  • 1
    "like it should" - did you mean "like I want it to"? :) – glomad Nov 11 '13 at 19:19
  • 1
    You are only missing, that Javascript is broken as hell, I had the exact same WTF moment about a week ago … :( – filmor Nov 11 '13 at 19:19
  • @VisioN Not really. I know how they work. I am wondering why they do not work like they should in this case. – Naftali Nov 11 '13 at 19:21
  • 1
    @Neal Well, I personally see that `j` is not initialized in the scope of `setTimeout` but in the scope of `timer` function, whereas in the second example you create an anonymous function, where you pass `i`, implicitly initialising `j` in the scope of closure. This creates and executes 3 functional blocks, setting 3 timeouts at once. – VisioN Nov 11 '13 at 19:27
  • @VisioN for loops don't have their own scopes? – Naftali Nov 11 '13 at 19:28
  • @Neal No, for loops don't have their own scope. In the next version of javascript there will be a `let` instead of `var` that will fix this. – some Nov 11 '13 at 19:28
  • @Neal No, variables defined in `for` loops are in the same scope the `for` loop is in. **REF:** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for#Parameters. – VisioN Nov 11 '13 at 19:31
  • 4
    It surprises me that someone who has answered over 1000 JavaScript/jQuery questions doesn't know how variable scope works in the language. – Blue Skies Nov 11 '13 at 19:41
  • @BlueSkies It did to me too. Hence I asked the question. [I learn new things every day](http://stackoverflow.com/questions/19914077/what-is-wrong-with-my-javascript-scope#comment29631015_19914115) – Naftali Nov 11 '13 at 19:42
  • @filmor—and you are missing that closures are a very powerful feature of the language. ;-) – RobG Aug 14 '14 at 23:02
  • @RobG Javascript is an curly-bracket language, all other curly-bracket languages I know of use block scope, so this was completely unexpected to me. This has nothing to do with closures, which are IMHO abused to regain block-scope in this case. – filmor Aug 16 '14 at 16:54
  • @filmor—it seemed more like a closure issue than not understanding variable scope. I don't see lack of block scope as an issue, if someone understands closures they should know how to fix that. Anyway, it seems *let* in [*ED 6*](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-let-and-const-declarations) will deliver function and block scope without disrupting current use of closures. It will be quite a while before it can be used on the general web though. – RobG Aug 18 '14 at 01:17

3 Answers3

37

Javascript has function scope. This means that

for(...) {
    var j = i;
}

is equivalent to

var j;
for(...) {
    j = i;
}

In fact, this is how Javascript compilers will actually treat this code. And, of course, this causes your little "trick" to fail, because j will be incremented before the function in setTimeout gets called, i.e. j now doesn't really do anything different than i, it's just an alias with the same scope.

If Javascript were to have block scope, your trick would work, because j would be a new variable within every iteration.

What you need to do is create a new scope:

for(var i = ...) {
    (function (j) {
        // you can safely use j here now
        setTimeout(...);
    })(i);
}
Ingo Bürk
  • 19,263
  • 6
  • 66
  • 100
6

The alternative the the IIFE is a function factory:

function timer() {
    for (var i = 0; i < 3; ++i) {
        setTimeout(createTimerCallback(i), 1000);
    }
}

function createTimerCallback(i) {
    return function() {
       alert(i);
    };
}

timer();

This being said, this is one of the most asked questions in the javascript tag. See:

Community
  • 1
  • 1
bfavaretto
  • 71,580
  • 16
  • 111
  • 150
  • Sooo hacky!! Why does javascript make you do these things? – Naftali Nov 11 '13 at 19:27
  • 2
    It's not hacky, it's just how scope works in this language. – bfavaretto Nov 11 '13 at 19:29
  • 1
    There's nothing hacky about it: Function scope versus block scope is a simple language feature which comes with effects like this. It just feels hacky as a lot of other languages have block scope which would prevent this behavior. – Ingo Bürk Nov 11 '13 at 19:29
3

An alternative is to use the (normally abused) keyword with:

function timer() {
    for (var i = 0; i < 3; ++i) {
        with({j: i}) {
            setTimeout(function () {
                alert(j);
            }, 1000);
        }
    }
}

timer();

It creates a new scope like functions do, but without the awkward syntax. I first saw it here: Are there legitimate uses for JavaScript's “with” statement?

Community
  • 1
  • 1
Izkata
  • 8,961
  • 2
  • 40
  • 50
  • 1
    I like that you gave a `with` example, but it doesn't create a scope "like functions do". There are important differences, which led to its removal from "strict mode". – Blue Skies Nov 11 '13 at 19:42
  • @BlueSkies Pseudo-scope, then. For the most part with practical use, it acts like it does. – Izkata Nov 11 '13 at 19:49