循环内的JavaScript闭合--简单的实际例子

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

它的输出结果是这样的。

我的值。3 我的值。3 我的价值3

而我希望它能输出。

我的值:0 我的值:1 我的值:2


当运行函数的延迟是由使用事件监听器造成的,也会出现同样的问题。

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>
// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

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

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

解决办法

好吧,问题是在你的每个匿名函数中的变量i,都与函数外的同一个变量绑定。

经典的解决方案。闭包

你要做的是将每个函数中的变量绑定到函数外的一个单独的、不变的值。

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++) {
  // and now let's run each one to see
  funcs[j]();
}

由于在JavaScript中没有块的范围--只有函数的范围--通过在一个新的函数中包装函数的创建,你可以确保"i"的值保持你的意图。


2015解决方案:forEach

随着Array.prototype.forEach函数的相对普及(2015年),值得注意的是,在那些主要涉及对数组值进行迭代的情况下,.forEach()提供了一种干净、自然的方式来为每次迭代获得一个独特的闭合。也就是说,假设你有某种包含值的数组(DOM引用、对象,等等),并且出现了为每个元素设置特定的回调的问题,你可以这样做。

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帖子](http://www.2ality.com/2015/02/es6-scoping.html)作为一个很好的信息来源

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

但请注意,IE9-IE11和Edge 14之前的版本支持 "let",但对上述情况的处理是错误的(它们不会每次都创建一个新的 "i",所以上述所有函数都会像我们使用 "var "那样记录3次)。Edge 14终于把它弄对了。

评论(23)

试试吧。

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的最新答案是现在做这种事的最好方法。还有lo-dash/underscore'的_.partial,当你不需要或不想搞乱bind'的thisArg时。

评论(3)

你原来的例子不成功的原因是,你在循环中创建的所有闭包都引用了同一个框架。实际上,一个对象上有3个方法,只有一个i变量。它们都打印出相同的值。

评论(0)