协慌网

登录 贡献 社区

循环内的 JavaScript 闭包 - 简单实用的例子

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
}

它输出:

我的价值:3
我的价值:3
我的价值:3

而我希望它输出:

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


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

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.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

... 或异步代码,例如使用 Promises:

// 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都绑定到函数之外的同一个变量。

经典解决方案:闭包

你想要做的是将每个函数中的变量绑定到函数之外的一个单独的,不变的值:

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 引用,对象等等),并且设置了特定于每个元素的回调问题,你可以这样做:

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)是最新版本的 JavaScript,现在开始在许多常绿浏览器和后端系统中实现。还有像Babel这样的转发器会将 ES6 转换为 ES5,以允许在旧系统上使用新功能。

ES6 引入了新的letconst关键字,其范围与基于var的变量不同。例如,在一个带有基于let的索引的循环中,循环中的每次迭代都会有一个新值i ,其中每个值都在循环中作用域,因此您的代码将按预期工作。有很多资源,但我建议将2ality 的区块范围发布作为一个很好的信息来源。

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

但是要注意,在 Edge 14 支持之前 IE9-IE11 和 Edge let但却出现上述错误(它们不会每次都创建一个新的i ,因此上面的所有函数都会记录 3,就像我们使用var )。 Edge 14 最终做对了。

尝试:

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

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

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]();
}

UPDATE

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

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]();
}