Scoping rules for variables initialized in a for loop [duplicate]

旧巷老猫 提交于 2019-12-06 02:03:59
Fabrício Matté

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!