Frage

Possible Duplicate:
Javascript closure inside loops - simple practical example

I'm playing around with setTimeout in a project of mine in order to throttle the adding of elements to the DOM (so UI won't freeze during page loading). However, I've encountered something a bit puzzling to me. Given this code:

for(var i = 0; i < 5; i++) {
    var j = i + 10;
    console.log("i is: " + i + " j is: " + j);
    setTimeout(function() {
        console.log("in timeout i is: " + i + " j is: " + j);
    }, i * 1000);
}

I get the following output:

i is: 0 j is: 10
i is: 1 j is: 11
i is: 2 j is: 12
i is: 3 j is: 13
i is: 4 j is: 14
in timeout i is: 5 j is: 14
in timeout i is: 5 j is: 14
in timeout i is: 5 j is: 14
in timeout i is: 5 j is: 14
in timeout i is: 5 j is: 14

That the value of i in the timeout is 5 is obvious since i is scoped in the for loop initialization. However, how come j is 14 for all timeout outputs? I would have thought that j would have output 10, 11, 12, 13, 14 in the timeout since it is scoped within the loop. How could I achieve that result?

War es hilfreich?

Lösung

That is because, in JavaScript, var has function scope.

var declarations will be hoisted up to the top of the current execution context. That is, if it is inside of a function, the var will be the scoped inside the function's execution context, otherwise the program (global) execution context.

ECMAScript 2015 (a.k.a. ES6) introduces let which lets you create block scope vars, but as it is not widely supported I'll just leave the link for reference.

An workaround, to still use var and have it "scoped" inside the loop, is to create a new execution context, also know as closure:

function callbackFactory(i, j) {
    // Now `i` and `j` are scoped inside each `callbackFactory` execution context.
    return function() { // This returned function will be used by the `setTimeout`.
       // Lexical scope (scope chain) will seek up the closest `i` and `j` in parent
       // scopes, that being of the `callbackFactory`'s scope in which this returned
       // function has been initialized.
       console.log("in timeout i is: " + i + " j is: " + j);
    };
}
for(var i = 0; i < 5; i++) {
    var j = i + 10;
    console.log("i is: " + i + " j is: " + j);
    setTimeout( callbackFactory(i, j), i * 1000);
}

As I scoped both i and j inside the callback scope, they will return the same values inside the setTimeout than they had when they were passed to callbackFactory.

See Live demo.

Another way to do the same thing is to create an IIFE inside the for loop. This is usually simpler to read but JS(H|L)int will yell at you. ;) This is because creating functions inside a loop is considered bad for performance.

for(var i = 0; i < 5; i++) {
    var j = i + 10;
    console.log("i is: " + i + " j is: " + j);
    (function(i, j) { // new execution context created for each iteration
        setTimeout(function() {
            console.log("in timeout i is: " + i + " j is: " + j);
        }, i * 1000);
    }(i, j)); // the variables inside the `for` are passed to the IIFE
}

Above I've created a new execution context inside the for in each iteration. (Demo)

Mixing the first approach (callbackFactory) with the IIFE above, we could even make a 3rd option:

for(var i = 0; i < 5; i++) {
    var j = i + 10;
    console.log("i is: " + i + " j is: " + j);
    setTimeout(function(i, j) {
        return function() {
            console.log("in timeout i is: " + i + " j is: " + j);
        };
    }(i, j), i * 1000);
}

This is simply using an IIFE in the place of the callbackFactory function. This doesn't seem very easy to read and still creates functions inside the for loop which is bad for performance, but just noting that this is also possible and works.

These 3 approaches are very commonly seen in the wild. =]


Oh, almost forgot to answer the main question. Just put the callbackFactory in the same scope as the for loop, then instead of scoping the i inside of it, let the scope chain seek the i of the outer scope:

(function() {
    var i, j;
    function callbackFactory(j) {
    // the `j` inside this execution context enters it as a formal parameter,
    // shadowing the outer `j`. That is, it is independent from the outer `j`.
    // You could name the parameter as "k" and use "k" when logging, for example.
        return function() {
           // Scope chain will seek the closest `j` in parent scopes, that being
           // the one from the callbackFactory's scope in which this returned
           // function has been initialized.
           // It will also seek up the "closest" `i`,
           // which is scoped inside the outer wrapper IIFE.
           console.log("in timeout i is: " + i + " j is: " + j);
        };
    }
    for(i = 0; i < 5; i++) {
        j = i + 10;
        console.log("i is: " + i + " j is: " + j);
        setTimeout( callbackFactory(j), i * 1000);
    }
}());
/* Yields:
i is: 0 j is: 10  
i is: 1 j is: 11  
i is: 2 j is: 12  
i is: 3 j is: 13  
i is: 4 j is: 14  
in timeout i is: 5 j is: 10  
in timeout i is: 5 j is: 11  
in timeout i is: 5 j is: 12  
in timeout i is: 5 j is: 13  
in timeout i is: 5 j is: 14 */

Fiddle

Note that I've moved the i and j declarations to the top of the scope solely for readability. It has the same effect as for (var i = [...], which would be hoisted up by the interpreter.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top