循环内的 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>

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

// 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 的块定义范围文章作为大量信息来源。

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

但是请注意,IE9-IE11 和 Edge 14 之前的 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时,还会有破折号 / 下划线的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]();
}

更新

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

使用立即调用函数表达式 ,这是封装索引变量的最简单,最易读的方法:

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变量。那么,为什么不每次迭代都创建一个新的局部变量,而让内部函数引用呢?

//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

重要提示:JavaScript 没有阻止范围。随块引入的变量的作用域为包含的函数或脚本,并且设置它们的效果在块本身之外仍然存在。换句话说,block 语句不会引入作用域。尽管 “独立” 块是有效的语法,但是您不希望在 JavaScript 中使用独立块,因为如果您认为独立块在 C 或 Java 中的作用类似于此类块,则它们不会像您想的那样工作。

重申重点:

JavaScript 没有阻止范围。块引入的变量的作用域为包含的函数或脚本

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

//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 中,内部函数可以访问外部变量,因为内部作用域 “包围” 了外部作用域。

关闭

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

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

在包装函数内部创建内部函数会为内部函数提供一个只有其才能访问的私有环境,即 “闭包”。因此,每次调用 wrapper 函数时,我们都会使用其自己的独立环境创建一个新的内部函数,以确保ilocal变量不会发生碰撞和彼此覆盖。进行一些次要的优化可以得出许多其他 SO 用户给出的最终答案:

//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关键字创建块作用域变量:

//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来设置这样的循环作用域变量:

var funcs = [];

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

val然后将指向特定于该循环特定循环的对象,并将返回正确的值,而无需附加的闭合符号。这显然大大简化了这个问题。

constlet类似,但有一个额外的限制,即变量名称在初始赋值后不能反弹到新引用。

现在,针对那些针对最新版本浏览器的浏览器提供了支持。最新的 Firefox,Safari,Edge 和 Chrome 当前支持const / let 。它在 Node 中也受支持,您可以利用 Babel 等构建工具在任何地方使用它。您可以在这里看到一个有效的示例: http : //jsfiddle.net/ben336/rbU4t/2/

此处的文档:

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

换句话说,函数中的i是在执行函数时绑定的,而不是在创建函数时绑定的。

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

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

只是以为我会添加一个解释以便清楚。对于解决方案,就我个人而言,我会选择 Harto,因为这是从此处的答案中最不言自明的方式。发布的任何代码都可以使用,但是我不得不选择一个关闭工厂,而不必编写大量注释来解释为什么我要声明一个新变量(Freddy 和 1800's)或具有怪异的嵌入式关闭语法(apphacker)。

您需要了解的是 javascript 中变量的范围是基于该函数的。这与在块范围内使用 c#相比,是一个重要的区别,只需将变量复制到 for 内部即可。

将其包装在一个评估返回函数的函数中(例如 apphacker 的答案)可以解决问题,因为变量现在具有函数作用域。

还有一个 let 关键字代替 var,它将允许使用块范围规则。在那种情况下,在 for 中定义一个变量就可以解决问题。就是说,由于兼容性,let 关键字不是实际的解决方案。

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

这是该技术的另一种变体,类似于 Bjorn(apphacker)的技术,它使您可以在函数内部分配变量值,而不是将其作为参数传递,这有时可能更清楚:

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

请注意,无论使用哪种技术, index变量都将成为一种静态变量,绑定到内部函数的返回副本上。即,在两次调用之间保留对其值的更改。可能非常方便。

这描述了在 JavaScript 中使用闭包的常见错误。

一个函数定义了一个新的环境

考虑:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

每次调用makeCounter{counter: 0}创建一个新对象。另外, obj创建一个新的obj副本以引用该新对象。因此, counter1counter2彼此独立。

闭包循环

在循环中使用闭包非常棘手。

考虑:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

请注意, counters[0]counters[1] 不是独立的。实际上,它们在相同的obj

这是因为可能出于性能原因,在循环的所有迭代中仅共享obj一个副本。即使{counter: 0}在每次迭代中创建一个新对象,该obj的相同副本也将使用对最新对象的引用进行更新。

解决方案是使用另一个辅助函数:

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

之所以可行,是因为直接在函数作用域中的局部变量以及函数自变量在输入时被分配了新副本。

有关详细讨论,请参见JavaScript 封闭陷阱和用法