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

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


2324
2018-04-15 06:06


起源


你确定你不想要 funcs 如果你使用数字索引,是一个数组?只是一个抬头。 - DanMan
这真是令人困惑的问题。这个 文章帮助我理解它。也可以帮助别人。 - user3199690
另一个简单和解释的解决方案:1) 嵌套函数 可以访问“上面”的范围; 2) 一个 关闭 解...“闭包是一个有权访问父作用域的函数,即使在父函数关闭后也是如此”。 - Peter Krauss
请参阅此链接以获得更好的Unserstanding javascript.info/tutorial/advanced-functions - Saurabh Ahuja
在 ES6,一个简单的解决方案是声明变量 一世 同 让,它的范围是循环体。 - Tomas Nikodym


答案:


好吧,问题是变量 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,现在开始在许多常绿浏览器和后端系统中实现。也有像 巴别塔 这会将ES6转换为ES5,以允许在旧系统上使用新功能。

ES6引入了新的 let 和 const 范围与...不同的关键字 var基于变量。例如,在带有a的循环中 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最终做对了。


1799
2018-04-15 06:18



不 function createfunc(i) { return function() { console.log("My value: " + i); }; } 仍然关闭,因为它使用变量 i? - アレックス
不幸的是,这个答案已经过时,没有人会在底部看到正确答案 - 使用 Function.bind() 现在绝对是可取的,请参阅 stackoverflow.com/a/19323214/785541。 - Wladimir Palant
@Wladimir:你的建议 .bind() 是 “正确答案” 不对。他们每个人都有自己的位置。同 .bind() 你不能绑定参数而不绑定 this 值。你也得到了一份 i 没有在调用之间变异的能力的论证,有时是需要的。所以它们是完全不同的结构,更不用说了 .bind() 实施历史上一直很慢。当然,在简单的例子中,任何一个都可以工作,但是闭包是一个需要理解的重要概念,这就是问题所在。 - cookie monster
请停止使用这些for-return功能黑客,使用[] .forEach或[] .map代替,因为它们避免重复使用相同的范围变量。 - Christian Landgren
@ChristianLandgren:只有在迭代一个数组时才有用。这些技术不是“黑客”。他们是必不可少的知识。


尝试:

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's 关于使用的最新答案 .bind 是现在做这种事情的最好方法。还有lo-dash / underscore _.partial 当你不需要或想要惹麻烦时 bindthisArg


340
2018-04-15 06:10



关于这个的任何解释 }(i)); ? - aswzen
@aswzen我认为它通过了 i 作为论点 index 到功能。 - Jet Blue


另一种尚未提及的方法是使用 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]();
}


310
2017-10-11 16:41



这也是我现在所做的,我也喜欢lo-dash / underscore _.partial - Bjorn Tipling
.bind() ECMAScript 6的功能将大部分已经过时。此外,这实际上每次迭代创建两个函数。首先是匿名,然后是生成的 .bind()。那么更好的用法是在循环之外创建它 .bind() 在里面。
这不会触发JsHint - 不要在循环中创建函数。 ?我也走了这条路,但是在运行代码质量工具之后就不行了.. - mekdev
@squint @mekdev - 你们两个都是对的。我最初的例子很快就是为了演示如何 bind 用来。我根据你的建议添加了另一个例子。 - Aust
我认为不是在两个O(n)循环上浪费计算,而是为(var i = 0; i <3; i ++){log.call(this,i); } - user2290820


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

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中的任何异步功能中使用。


234
2017-10-11 18:23



为了进一步提高代码的可读性并避免混淆 i 是什么,我将函数参数重命名为 index。 - Kyle Falconer
您将如何使用此技术来定义数组 funcs中 在原始问题中描述? - Nico
@Nico与原始问题中显示的方式相同,除非您使用 index 代替 i。 - JLRishe
@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); } - Nico
@Nico在OP的特殊情况下,他们只是迭代数字,所以这不是一个很好的例子 .forEach(),但很多时候,当一个人开始使用数组时, forEach()是一个不错的选择,如: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; }); - JLRishe


派对迟到了,但我今天正在探讨这个问题,并注意到许多答案并没有完全解决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没有块范围。使用块引入的变量的范围限定为包含函数或脚本,并且设置它们的效果将持续超出块本身。换句话说,块语句不引入范围。虽然“独立”块是有效的语法,但您不希望在JavaScript中使用独立块,因为如果您认为它们在C或Java中执行类似块的操作,则它们不会按照您的想法执行。

重申强调:

JavaScript没有块范围。使用块引入的变量的范围限定为包含函数或脚本

我们可以通过检查看到这一点 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中,内部函数可以访问外部变量,因为内部作用域“包围”外部作用域。

Closures

这也意味着内部函数“保持”外部变量并使它们保持活动,即使外部函数返回。为了利用这一点,我们创建并调用一个包装器函数,纯粹是为了创建一个新的范围,声明 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]();
}

在包装器函数内部创建内部函数为内部函数提供了一个只有它才能访问的私有环境,即“闭包”。因此,每次我们调用包装函数时,我们都会创建一个新的内部函数,它具有自己独立的环境,确保了 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]();
}

看看它现在有多容易!有关更多信息,请参阅 这个答案,我的信息是基于。


127
2018-04-10 09:57



我也喜欢你如何解释IIFE的方式。我在找那个。谢谢。 - CapturedTree
现在有一个在JavaScript中使用块的作用域 let 和 const 关键字。如果这个答案扩大到包括那个,那么在我看来它将更具全球性。 - Tiny Giant
@TinyGiant肯定的是,我添加了一些信息 let 并链接了一个更完整的解释 - woojoo666
@ woojoo666你的回答是否也适用于在循环中调用两个交替的URL,如下所示: i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (可以用getelementbyid替换window.open()......) - nutty about natty
@nuttyaboutnatty抱歉这么晚的回复。看起来您的示例中的代码似乎不起作用。你没有使用 i 在你的超时函数中,所以你不需要一个闭包 - woojoo666


随着ES6现在得到广泛支持,这个问题的最佳答案已经改变。 ES6提供了 let 和 const 这个确切情况的关键字。我们可以使用,而不是搞乱闭包 let 设置一个循环范围变量,如下所示:

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

val 然后将指向一个特定于该循环的特定转向的对象,并将返回正确的值而不使用额外的闭包表示法。这显然简化了这个问题。

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

现在,浏览器支持针对最新版本的浏览器。 const/let 目前最新的Firefox,Safari,Edge和Chrome都支持。 Node也支持它,你可以利用像Babel这样的构建工具在任何地方使用它。你可以在这里看到一个有效的例子: http://jsfiddle.net/ben336/rbU4t/2/

文件在这里:

但请注意,在Edge 14支持之前IE9-IE11和Edge let 但得到上述错误(他们没有创造新的 i 每一次,所以上面的所有函数都会像我们使用的那样记录3 var)。 Edge 14最终做对了。


121
2018-05-21 03:04



不幸的是,'let'仍未完全支持,特别是在移动设备中。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... - MattC
截至2016年6月, 让 除iOS OS,Opera Mini和Safari 9外,所有主要浏览器版本均支持。常青浏览器支持它。 Babel会正确地将其转换为保持预期的行为而不打开高合规性模式。 - Dan Pantry
@DanPantry是的关于更新的时间:)更新以更好地反映当前的状态,包括添加const,doc链接和更好的兼容性信息。 - Ben McCormick
这不是为什么我们使用babel来转换我们的代码,所以不支持ES6 / 7的浏览器可以理解发生了什么? - pixel 67


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

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

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

我想我会添加一个清晰的解释。对于解决方案,个人而言,我会选择Harto,因为从这里的答案来看,这是最明智的方式。发布的任何代码都可以使用,但是我选择封闭工厂而不必编写一堆注释来解释为什么我要声明一个新变量(Freddy和1800's)或者有奇怪的嵌入式闭包语法(apphacker)。


77
2018-04-15 06:48





你需要了解的是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]();                        
}

61
2018-04-15 06:25



@nickf哪个浏览器?正如我所说,它有兼容性问题,我的意思是严重的兼容性问题,比如我认为IE不支持。 - eglasius
@nickf是的,请查看此参考: developer.mozilla.org/En/New_in_JavaScript_1.7 ...检查let definitions部分,循环内部有一个onclick示例 - eglasius
@nickf嗯,实际上你必须明确指定版本:<script type =“application / javascript; version = 1.7”/> ...我实际上并没有因为IE的限制而在任何地方使用它,它只是不是实际的 :( - eglasius
也可以看看 哪些浏览器目前支持javascript的'let'关键字? - rds
不要使用let - regisbsb


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

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

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


49
2017-08-07 08:45



谢谢你的解决方案。但我想问为什么这样做,但交换 var 线和 return 线不行?谢谢! - midnite
@midnite如果你换了 var 和 return 然后在返回内部函数之前不会分配变量。 - Boann


这描述了在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  也被创建以引用新对象。从而, counter1 和 counter2 是彼此独立的。

循环中的闭包

在循环中使用闭包很棘手。

考虑:

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关闭陷阱和使用


45
2018-04-20 09:59





最简单的解决方案是,

而不是使用:

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

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

提醒“2”,共3次。这是因为在for循环中创建的匿名函数,共享相同的闭包,并在该闭包中,值为 i 是一样的。使用它来防止共享关闭:

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

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

这背后的想法是,用for封装整个for循环体 IIFE (立即调用函数表达式)并传递 new_i 作为参数并将其捕获为 i。由于匿名函数是立即执行的,所以 i 对于匿名函数内定义的每个函数,value是不同的。

这个解决方案似乎适合任何这样的问题,因为它需要对遇到此问题的原始代码进行最小的更改。事实上,这是设计,它应该不是一个问题!


41
2018-06-25 14:21



一次读一本类似的东西。我也更喜欢这个,因为你不必触及你现有的代码(同样多),一旦你学会了自我调用函数模式,你就会明白为什么这样做:在新创建的函数中捕获该变量范围。 - DanMan
@DanMan谢谢。自调用匿名函数是处理javascript缺少块级变量范围的非常好的方法。 - Kemal Dağ
自我调用或自我调用不适合这种技术, IIFE (立即调用函数表达式)更准确。参考: benalman.com/news/2010/11/... - jherax