循环内的javascript闭包——简单的实践示例

JavaScript closure inside loops – simple practical example

1
2
3
4
5
6
7
8
9
var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value:" + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

它输出:

My value: 3
My value: 3
My value: 3

但是我希望它输出:

My value: 0
My value: 1
My value: 2

当使用事件侦听器导致函数运行延迟时,也会出现同样的问题:

1
2
3
4
5
6
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value:" + i);                  // each should log its value.
  });
}
1
2
3
<button>0</button>
<button>1</button>
<button>2</button>

…或异步代码,例如使用承诺:

1
2
3
4
5
6
// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for(var i = 0; i < 3; i++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

这个基本问题的解决方案是什么?


问题是,变量i在每个匿名函数中都绑定到了函数外部的同一个变量。

经典解决方案:闭包

您要做的是将每个函数中的变量绑定到函数外部一个独立的、不变的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
var funcs = [];

function createfunc(i) {
    return function() { console.log("My value:" + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

由于在javascript中没有块作用域(只有函数作用域),通过将函数创建包装到新函数中,可以确保"i"的值保持原样。

2015解决方案:foreach

随着Array.prototype.forEach函数的相对广泛可用性(2015年),值得注意的是,在那些主要涉及对一系列值进行迭代的情况下,.forEach()为每个迭代提供了一种干净、自然的方法来获得明显的结束。也就是说,假设您有一些包含值(dom引用、对象等)的数组,并且设置每个元素的回调时出现问题,您可以这样做:

1
2
3
4
5
6
7
8
var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

其思想是,与.forEach循环一起使用的回调函数的每次调用都将是它自己的闭包。传递给该处理程序的参数是特定于迭代的特定步骤的数组元素。如果在异步回调中使用,它不会与在迭代的其他步骤中建立的任何其他回调冲突。

如果您正好在jquery中工作,那么$.each()函数为您提供了类似的功能。

ES6解决方案:let

ecmascript 6(es6)引入了新的letconst关键字,它们的作用域与基于var的变量不同。例如,在具有基于let的索引的循环中,通过循环的每个迭代都将有一个新的i值,其中每个值都在循环中有作用域,因此您的代码将按预期工作。有很多资源,但我建议2Ality的block scoping post作为一个很好的信息源。

1
2
3
4
5
for (let i = 0; i < 3; i++) {
    funcs[i] = function() {
        console.log("My value:" + i);
    };
}

不过,要注意,IE9-IE11和Edge Prior to Edge 14支持let,但上述错误(它们每次都不会创建新的i,因此上面的所有功能都会像使用var时一样记录3)。第14边终于弄好了。


尝试:

1
2
3
4
5
6
7
8
9
10
11
12
var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value:" + index);
        };
    }(i));
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

编辑(2014):

就我个人而言,我认为@aust最近关于使用.bind的回答是目前最好的方法。当你不需要或不想与bindthisArg发生冲突时,还有lo dash/underline的_.partial


另一种尚未提及的方法是使用Function.prototype.bind

1
2
3
4
5
6
7
8
9
var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

更新

正如@squint和@mekdev所指出的,首先在循环外部创建函数,然后在循环内部绑定结果,可以获得更好的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

for (var j = 0; j < 3; j++) {
  funcs[j]();
}


使用立即调用的函数表达式,最简单、最易读的方法来包含索引变量:

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 3; i++) {

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

它将迭代器i发送到匿名函数中,我们将其定义为index。这就创建了一个闭包,其中保存了变量i,以供以后在IIFE中的任何异步功能中使用。


参加聚会有点晚,但我今天正在研究这个问题,注意到许多答案并没有完全解决JavaScript如何处理范围的问题,这本质上就是归根结底。

正如其他许多人提到的,问题是内部函数引用的是同一个i变量。那么,为什么我们不在每次迭代中创建一个新的局部变量,并拥有内部函数引用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>
'
+ msg + '
</p>'
;};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value:" + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

和以前一样,每个内部函数输出分配给i的最后一个值,现在每个内部函数只输出分配给ilocal的最后一个值。但每个迭代不应该有自己的ilocal吗?

结果,这就是问题所在。每个迭代都共享相同的范围,所以第一次迭代之后的每个迭代都只是覆盖ilocal。来自MDN:

Important: JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script, and the effects of setting them persist beyond the block itself. In other words, block statements do not introduce a scope. Although"standalone" blocks are valid syntax, you do not want to use standalone blocks in JavaScript, because they don't do what you think they do, if you think they do anything like such blocks in C or Java.

再次强调:

JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script

我们可以通过在每次迭代中声明之前检查ilocal来看到这一点:

1
2
3
4
5
6
7
8
9
10
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>
'
+ msg + '
</p>'
;};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

这就是为什么这个bug如此棘手的原因。即使您正在重新定义一个变量,javascript也不会抛出错误,JSlint甚至不会抛出警告。这也是解决这一问题的最佳方法是利用闭包的原因,闭包本质上是指在JavaScript中,内部函数可以访问外部变量,因为内部范围"包含"了外部范围。

Closures

这也意味着内部函数"保留"外部变量并使它们保持活动,即使外部函数返回。为了利用这个功能,我们创建并调用一个包装函数来创建一个新的作用域,在新的作用域中声明ilocal,并返回一个使用ilocal的内部函数(下面有更多解释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>
'
+ msg + '
</p>'
;};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value:" + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

在包装函数内创建内部函数会给内部函数一个只有它可以访问的私有环境,即"闭包"。因此,每次调用包装器函数时,我们都会使用它自己的独立环境创建一个新的内部函数,以确保ilocal变量不会相互碰撞和覆盖。一些小的优化给出了许多其他用户给出的最终答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>
'
+ msg + '
</p>'
;};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value:" + ilocal);
    };
}

更新

现在ES6已经成为主流,我们可以使用新的let关键字来创建块范围变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>
'
+ msg + '
</p>'
;};

var funcs = {};
for (let i = 0; i < 3; i++) { // use"let" to declare"i"
    funcs[i] = function() {
        console.log("My value:" + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use"var" here without issue
    funcs[j]();
}

看看现在有多简单!有关更多信息,请参阅此答案,我的信息基于此答案。


随着ES6得到了广泛的支持,这个问题的最佳答案已经改变了。ES6为这种具体情况提供了letconst关键字。我们可以使用let来设置一个这样的循环范围变量,而不是乱搞闭包:

1
2
3
4
5
6
var funcs = [];
for (let i = 0; i < 3; i++) {          
    funcs[i] = function() {            
      console.log("My value:" + i);
    };
}

然后,val将指向一个特定于该特定循环周期的对象,并返回正确的值,而不需要附加的结束符号。这显然大大简化了这个问题。

constlet相似,附加的限制是变量名不能在初始赋值后恢复到新的引用。

浏览器支持现在针对的是最新版本的浏览器。最新的火狐、Safari、Edge和Chrome目前支持const/let。它在节点中也受支持,您可以通过利用诸如babel之类的构建工具在任何地方使用它。您可以在这里看到一个工作示例:http://jsfiddle.net/ben336/rbu4t/2/

这里的医生:

  • 康斯特

不过,要注意,IE9-IE11和Edge Prior to Edge 14支持let,但上面的错误(它们不会每次创建新的i,因此上面的所有函数都会像使用var时一样记录3)。第14边终于弄好了。


另一种说法是,函数中的i是在执行函数时绑定的,而不是在创建函数时绑定的。

创建闭包时,i是对外部作用域中定义的变量的引用,而不是创建闭包时该变量的副本。它将在执行时进行评估。

大多数其他答案都提供了解决方法,通过创建另一个不会改变值的变量。

我只是想加一个解释来解释清楚。我个人认为,要想找到一个解决办法,我会选择哈托的方法,因为从这里的答案来看,这是最不言自明的方法。发布的任何代码都可以工作,但是我会选择关闭工厂,而不是写一堆注释来解释为什么我要声明一个新变量(Freddy和1800),或者有奇怪的嵌入式关闭语法(Apphacker)。


您需要了解的是,javascript中变量的范围是基于函数的。这比在有块范围的地方说c有一个重要的区别,只需将变量复制到for中的一个就可以了。

将它包装在一个函数中,该函数的计算结果将返回类似于Apphacker的答案的函数,这将达到目的,因为变量现在具有函数范围。

还有一个let关键字而不是var,它允许使用块范围规则。在这种情况下,在for中定义一个变量可以做到这一点。也就是说,由于兼容性的原因,let关键字不是一个实用的解决方案。

1
2
3
4
5
6
7
8
9
10
var funcs = {};
for (var i = 0; i < 3; i++) {
    let index = i;          //add this
    funcs[i] = function() {            
        console.log("My value:" + index); //change to the copy
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        
}