在for循环中初始化的变量的作用域规则

Emi*_*l H 3 javascript scoping settimeout

可能重复:
循环内部的Javascript闭包 - 简单实用的例子

我在我的一个项目中玩setTimeout以限制向DOM添加元素(因此UI在页面加载期间不会冻结).但是,我遇到了一些让我感到困惑的事情.鉴于此代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

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
Run Code Online (Sandbox Code Playgroud)

这价值i在超时为5是显而易见的,因为我是在用于环路初始化作用域.但是,j对于所有超时输出,14 怎么样?我原本以为它j会在超时中输出10,11,12,13,14,因为它在循环中作用域.我怎么能实现这个结果?

Fab*_*tté 7

那是因为,在JavaScript中,var具有功能范围.

var声明将被提升到当前执行上下文的顶部.也就是说,如果它在函数内部,var则将在函数的执行上下文中作用域,否则为程序(全局)执行上下文.

ECMAScript 2015(又名ES6)介绍let了允许您创建块范围变量,但由于它没有得到广泛支持,我将只留下链接以供参考.

一个解决方法,仍然使用var并在循环中"限定",是创建一个新的执行上下文,也称为闭包:

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);
}
Run Code Online (Sandbox Code Playgroud)

当我在回调范围内i和它j内部时,它们将返回与setTimeout传递到它们时相同的值callbackFactory.

查看现场演示.

另一种做同样事情的方法是在循环内创建一个IIFEfor.这通常更容易阅读,但JS(H | L)int会对你大喊大叫.;)这是因为在循环内创建函数被认为对性能有害.

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
}
Run Code Online (Sandbox Code Playgroud)

上面我for在每次迭代中创建了一个新的执行上下文.(演示)

将第一种方法(callbackFactory)与上面的IIFE 混合,我们甚至可以做出第三种选择:

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);
}
Run Code Online (Sandbox Code Playgroud)

这只是使用IIFE来代替callbackFactory功能.这似乎不是很容易阅读,仍然里面创建功能for回路这是不好的表现,只是指出,这也可能和工作.

这3种方法在野外很常见. =]


哦,差点忘了回答主要问题.只需将它callbackFactory放在与for循环相同的范围内,然后i让范围链寻找i外部范围:

(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 */
Run Code Online (Sandbox Code Playgroud)

小提琴

请注意,为了便于阅读,我已将声明ij声明移到了作用域的顶部.它具有与for (var i = [...]解释器提升的效果相同的效果.