循环内的JavaScript闭合–简单的实际示例


2816

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

编辑:在for infor of循环中也很明显:

const arr = [1,2,3];
const fns = [];

for(i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(f of fns){
  f();
}

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


55
funcs如果使用数字索引,您确定不想成为数组吗?只是抬头。
DanMan

23
这确实是一个令人困惑的问题。此文章帮助我理解它。可能也会帮助他人。
user3199690 2014年

4
另一个简单的解释解决方案:1)嵌套函数可以访问它们“上方”的范围;2)一个封闭溶液 ...“的封闭是具有访问父范围,即使在父功能已关闭的功能”。
彼得·克劳斯

2
请参阅此链接以获取更好的Unserstanding javascript.info/tutorial/advanced-functions
Saurabh Ahuja

35
ES6中,一个简单的解决方案是使用let声明变量i,该变量的作用域是循环的主体。
Tomas Nikodym

Answers:


2145

好吧,问题在于变量 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”的值保持预期。


ES5.1解决方案: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终于正确了。


7
还不function createfunc(i) { return function() { console.log("My value: " + i); }; }是因为使用变量而关闭了i吗?
アレックス

55
不幸的是,这个答案已经过时了,没有人会在底部看到正确的答案-使用Function.bind()绝对是可取的,请参见stackoverflow.com/a/19323214/785541
Wladimir Palant 2014年

81
@Wladimir:您的建议.bind()“正确答案”是不对的。他们每个人都有自己的位置。使用时,.bind()如果不绑定this值就不能绑定参数。此外,您还获得了i参数的副本,但无法在调用之间进行更改,有时这是必需的。因此,它们是完全不同的构造,更不用说.bind()实现历史上缓慢了。当然,在简单的示例中也可以用,但是闭包是一个重要的概念,需要理解,这就是问题所在。
cookie怪物

8
请停止使用这些for-return函数技巧,而改用[] .forEach或[] .map,因为它们避免了重复使用相同的作用域变量。
Christian Landgren 2015年

32
@ChristianLandgren:这仅在迭代数组时才有用。这些技术不是“黑客”。它们是必不可少的知识。

379

尝试:

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-破折号/下划线是_.partial当你不需要或不想要惹做bindthisArg


2
关于的任何解释}(i));
aswzen

3
@aswzen我认为它i作为index函数的参数传递。
蓝色

它实际上是在创建局部变量索引。
Abhishek Singh,

1
立即调用函数表达式,也称为IIFE。(i)是匿名函数表达式的参数,该参数立即被调用,并且索引从i开始设置。
鸡蛋

348

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


这几天我也是这么做的,我也喜欢lo-dash / underscore_.partial
Bjorn 2014年

17
.bind()ECMAScript 6功能将在很大程度上过时。此外,这实际上每次迭代创建两个函数。首先是匿名者,然后是由生成的匿名者.bind()。更好的用法是在循环外部创建它,然后.bind()在内部创建它。

5
@squint @mekdev-你们都是正确的。我最初的例子写得很快,以演示如何bind使用。根据您的建议,我添加了另一个示例。
2015年

5
我认为与其在两个O(n)循环上浪费计算,不如对(var i = 0; i <3; i ++){log.call(this,i); }
user2290820 2015年

1
.bind()会执行被接受的答案,以及PLUS摆弄this
niry

269

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

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


10
为了进一步提高代码的可读性并避免引起混淆i,我将功能参数重命名为index
Kyle Falconer 2014年

5
您将如何使用这种技术来定义原始问题中描述的数组函数
Nico 2014年

@Nico与原始问题中所示的方法相同,只不过您可以使用index代替i
JLRishe 2015年

@JLRishevar 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

1
@Nico在OP的特殊情况下,它们只是遍历数字,因此对于来说并不是一个好例子.forEach(),但是很多时候,当一个数组开始时,forEach()是一个不错的选择,例如:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
JLRishe 2015年

163

参加聚会的时间有点晚,但是我今天正在探讨这个问题,并注意到许多答案并没有完全解决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在每次迭代中声明之前进行检查来查看:

//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现在成为主流,我们现在可以使用new 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]();
}

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


我也喜欢您对IIFE方式的解释。我一直在寻找那个。谢谢。
CapturedTree

4
现在,发生了诸如使用letand const关键字在JavaScript中进行块范围界定的事情。如果将这个答案扩展到包括它在内,我认为这将在全球范围内变得更加有用。

@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()……)
关于

@nuttyaboutnatty很抱歉收到这么晚的回复。您的示例中的代码似乎无法正常工作。您没有使用i超时功能,因此不需要关闭
woojoo666

151

如今,ES6得到了广泛支持,对此问题的最佳答案已经改变。ES6 为此情况提供了letconst关键字。不用弄乱闭包,我们可以使用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浏览器,边缘和Chrome。它在Node中也受支持,您可以利用Babel等构建工具在任何地方使用它。您可以在此处看到一个有效的示例:http : //jsfiddle.net/ben336/rbU4t/2/

此处的文档:

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


不幸的是,“ let”仍然没有得到完全支持,尤其是在移动设备中。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…–
MattC

2
截至6月'16的,所有主流浏览器版本的iOS,除了Safari浏览器,Opera Mini的和Safari 9.长荣浏览器都支持支持。Babel将正确地进行转换,以保持预期的行为而无需打开高兼容模式。
Dan Pantry,2016年

@DanPantry是关于更新的时间:)已更新以更好地反映当前情况,包括添加const,文档链接和更好的兼容性信息。
本·麦考密克,

这不是为什么我们使用babel来翻译代码,以便不支持ES6 / 7的浏览器可以了解发生了什么吗?
像素

87

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

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

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

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


71

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


@nickf哪个浏览器?如我所说,它存在兼容性问题,这意味着严重的兼容性问题,例如我认为IE中不支持let。
eglasius

1
@nickf是的,请检查以下参考:developer.mozilla.org/En/New_in_JavaScript_1.7 ...检查let定义部分,循环内有一个onclick示例
eglasius

2
@nickf hmm,实际上您必须明确指定版本:<script type =“ application / javascript; version = 1.7” /> ...由于IE的限制,我实际上没有在任何地方使用过它,但这并不是实用:(
eglasius

您可以在此处查看浏览器对不同版本的支持es.wikipedia.org/wiki/Javascript
eglasius 2009年


59

这是该技术的另一种变体,类似于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变量都将成为一种静态变量,绑定到内部函数的返回副本上。即,在两次调用之间保留对其值的更改。可能非常方便。


谢谢,您的解决方案有效。但是我想问为什么这行得通,但是交换var线路和return线路是行不通的?谢谢!
2013年

@midnite如果您进行了交换varreturn则在返回内部函数之前不会分配该变量。
Boann 2013年

53

这描述了在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 创建的新副本以引用新对象。因此,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);
  }
}

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


简要说明:在第一个闭包循环示例中,counter [0]和counters [1]不是独立的,并非出于性能原因。原因是要 var obj = {counter: 0};在执行任何代码之前先进行评估,如:MDN varvar声明,无论它们出现在何处,都将在执行任何代码之前进行处理。
Charidimos

50

最简单的解决方案是

而不是使用:

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

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

此解决方案似乎适合任何此类问题,因为它将需要对遭受此问题的原始代码进行最少的更改。实际上,这是设计使然,根本不成问题!


2
读一本书类似的东西。我也更喜欢这样做,因为您不必过多地触摸现有代码,并且一旦知道了自调用函数模式,就很明显为什么要这样做:将变量捕获在新创建的函数中范围。
DanMan

1
@DanMan谢谢。自调用匿名函数是解决javascript缺少块级变量范围的一种很好的方法。
2013年

3
自调用或自调用不是此技术的合适术语,IIFE(立即调用函数表达式)更为准确。参考:benalman.com/news/2010/11/…–
jherax

31

试试这个较短的

  • 没有数组

  • 没有额外的循环


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

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

http://jsfiddle.net/7P6EN/


1
您的解决方案似乎输出正确,但是不必要地使用了函数,为什么不只是console.log输出呢?最初的问题是关于创建具有相同闭包的匿名函数。问题是,由于它们只有一个闭包,所以每个i的值都相同。我希望你明白了。
凯末尔·达格

30

这是一个使用简单的解决方案forEach(可回溯到IE9):

var funcs = [];
[0,1,2].forEach(function(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: 0
My value: 1
My value: 2

27

OP显示的代码的主要问题是,i直到第二个循环才读取。为了演示,假设看到代码内部有错误

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

funcs[someIndex]执行之前,实际上不会发生该错误()。使用相同的逻辑,很明显,i直到这一点也不会收集的值。原始循环完成后,i++i其取值3会导致条件i < 3失败和循环结束。在这一点上,使用iis 3funcs[someIndex](),并被i评估时,每次为3。

为了克服这个问题,您必须评估i遇到的问题。请注意,这已经以funcs[i](其中有3个唯一索引)。有几种获取此值的方法。一种是将其作为参数传递给函数,此处已经以几种方式显示了该函数。

另一种选择是构造一个函数对象,该对象将能够覆盖变量。这样就可以做到

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};

23

JavaScript函数在声明时“关闭”它们可以访问的范围,并且即使该范围中的变量发生更改,也保留对该范围的访问。

var funcs = []

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

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

上面数组中的每个函数都关闭了全局范围(全局,仅因为这恰好是它们在其中声明的范围)。

稍后调用这些函数,记录i全局范围内的最新值。这就是关闭的魔力和挫败感。

“ JavaScript函数在声明它们的范围内关闭,并且即使该范围内的变量值发生更改,也保留对该范围的访问。”

使用let而不是var通过在每次for循环运行时创建一个新的作用域,为每个要关闭的函数创建一个单独的作用域来解决此问题。其他各种技术也可以通过其他功能来完成相同的任务。

var funcs = []

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

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

let使变量成为块作用域。块用花括号表示,但是在for循环的i情况下,在我们的情况下,初始化变量被视为在花括号中声明。)


1
在阅读此答案之前,我一直难以理解这个概念。它触及了一个非常重要的点–值i已设置为全局范围。当for循环完成运行时,的全局值i现在为3。因此,每当在数组中调用该函数时(使用funcs[j]),该i函数中的in便会引用全局i变量(即3)。
Modermo '17

13

在阅读了各种解决方案之后,我想补充一点,那些解决方案起作用的原因是依赖于范围链的概念。这是JavaScript在执行期间解析变量的方式。

  • 每个函数定义形式,包括所有的局部变量的范围内声明的通过var和它的arguments
  • 如果我们在另一个(外部)函数中定义了内部函数,则这将形成一个链,并将在执行期间使用
  • 执行函数时,运行时通过搜索作用域链来评估变量。如果可以在链的某个点找到变量,它将停止搜索并使用它,否则它将一直持续到达到属于的全局范围为止window

在初始代码中:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

funcs被执行时,作用域链会function inner -> global。由于i无法在中找到变量function inner(既未使用声明,var也未作为参数传递),因此它将继续搜索,直到i最终在的全局范围中找到的值window.i

通过将其包装在外部函数中,或者像harto那样显式定义一个辅助函数,或者像Bjorn那样使用匿名函数:

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

如果funcs得到执行,现在的作用域链会function inner -> function outer。这个时间i可以在外部函数的作用域中找到,该作用域在for循环中执行了3次,每次都有i正确绑定的值。window.i内部执行时将不使用值。

可以在此处找到更多详细信息
其中包括在循环中创建闭包的常见错误以及我们为什么需要闭包和性能考虑。


我们很少真正地编写此代码示例,但我认为它是理解基本原理的一个很好的例子。一旦我们确定了作用域以及它们如何链接在一起,就可以更清楚地了解为什么其他“现代”方式Array.prototype.forEach(function callback(el) {})自然起作用:传入的回调自然形成包装作用域,并在每次迭代中正确绑定el forEach。因此,回调中定义的每个内部函数都将能够使用正确的el
wpding

13

借助ES6的新功能,可以管理块级范围:

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

OP问题中的代码被替换letvar


const提供相同的结果,并且当变量的值不变时应使用。但是,const在Firefox中错误地实现了for循环的初始化程序内部的使用,并且尚未修复。它不是在块内部声明,而是在块外部声明,这导致对变量的重新声明,进而导致错误。let在Firefox中可以正确实现内部初始化程序的使用,因此无需担心。

10

令我惊讶的是,还没有人建议使用该forEach函数来更好地避免(重新)使用局部变量。实际上,for(var i ...)由于这个原因,我不再使用了。

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

//编辑以使用forEach而不是地图。


3
.forEach()如果您实际上没有映射任何内容,则是一个更好的选择,Daryl建议您在发布前7个月,因此没有什么奇怪的。
JLRishe

这个问题不是关于遍历数组的问题
jherax

好吧,他想创建一个函数数组,此示例说明了如何在不涉及全局变量的情况下做到这一点。
Christian Landgren 2015年

9

这个问题确实显示了JavaScript的历史!现在,我们可以避免使用箭头功能进行块作用域定义,而可以使用Object方法直接从DOM节点处理循环。

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>


8

您的原始示例无效的原因是您在循环中创建的所有闭包都引用了同一框架。实际上,对一个对象具有3个方法而只有一个i变量。它们都打印出相同的值。


8

首先,了解此代码有什么问题:

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
}

在这里,当funcs[]初始化数组时,i将对其进行递增,funcs数组将被初始化,并且数组的大小将func变为3,因此i = 3,。现在,当funcs[j]()调用时,它再次使用变量i,该变量已经增加到3。

现在解决这个问题,我们有很多选择。以下是其中两个:

  1. 我们可以初始化ilet或初始化一个新的变量indexlet和使其等于i。因此,在进行调用时,index将使用它,并且其范围将在初始化后结束。并进行调用,index将再次初始化:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
  2. 其他选项可以是引入一个tempFunc返回实际功能的:

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

8

使用闭包结构,这样可以减少多余的for循环。您可以在单个for循环中执行此操作:

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

7

我们将检查,varlet 一一声明时实际发生的情况。

案例1使用var

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

现在,按F12键打开chrome控制台窗口,然后刷新页面。扩展数组中的每3个函数。您将看到一个名为.Expand 的属性。您将看到一个名为的数组对象,将其展开。您会发现在对象中声明了一个值为3 的属性。[[Scopes]]"Global"'i'

在此处输入图片说明

在此处输入图片说明

结论:

  1. 当您'var'在函数外部使用变量声明时,它将变为全局变量(您可以通过在控制台窗口中键入iwindow.i进行检查,它将返回3)。
  2. 除非您调用这些函数,否则声明的匿名函数将不会调用并检查函数内部的值。
  3. 调用函数时,console.log("My value: " + i)从其Global对象获取值并显示结果。

案例2:使用let

现在更换'var''let'

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

做同样的事情,转到范围。现在您将看到两个对象"Block""Global"。现在展开Block对象,您将看到在其中定义了“ i”,奇怪的是,对于每个函数,其值是否i不同(0、1、2)。

在此处输入图片说明

结论:

当您'let'甚至在函数外部但在循环内部使用变量声明时,此变量将不是全局变量,它将变为Block仅适用于同一函数的级别变量。这就是为什么我们得到i不同值的原因对于每个函数,当我们调用函数时。

有关更近距离工作原理的更多详细信息,请浏览精彩的视频教程https://youtu.be/71AtaJpJHw0


4

您可以对数据列表使用声明性模块,例如query-js(*)。在这些情况下,我个人发现声明式方法不足为奇

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

然后,您可以使用第二个循环并获得预期的结果,或者您可以执行

funcs.iterate(function(f){ f(); });

(*)我是query-js的作者,因此偏向于使用它,因此不要只将我的话作为对上述库的建议:)


1
我希望解释一下否决票。该代码解决了手头的问题。知道如何潜在地改进代码很有价值
Rune FS

1
什么Query.range(0,3)啊 这不是此问题的标签的一部分。此外,如果您使用第三方库,则可以提供文档的链接。
jherax

1
@jherax这些是或当然有明显的改进。感谢您的评论。我本可以宣誓已经存在链接。有了这个帖子,我想是没有意义的:)。保留它的最初想法是因为我不是在尝试使用自己的库,而是在声明性的想法。但是在hinsight中,我完全同意链接应该存在
符文FS

4

我更喜欢使用forEach函数,该函数在创建伪范围时具有自己的闭包:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

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

这看起来比其他语言的范围丑陋,但是恕我直言,它不如其他解决方案那么可怕。


更喜欢什么?这似乎是对其他答案的评论。它根本没有解决实际的问题(因为您没有分配函数,以后将在任何地方调用)。
昆汀2015年

它与上述问题完全相关:如何安全地进行迭代而没有关闭问题
Rax Wunter 2015年

现在看来,它与公认的答案没有太大的不同。
昆汀2015年

否。在接受的答案中,建议使用“某些数组”,但是我们在答案中处理的是范围,这是完全不同的,不幸的是,在js中没有好的解决方案,所以我的答案是尝试解决以良好的实践方式解决问题
Rax Wunter 2015年

@Quentin我建议在研究之前研究解决方案
Rax Wunter 2015年

4

还有另一种解决方案:无需创建另一个循环,只需将绑定this到return函数即可。

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

通过绑定,也解决了问题。


3

许多解决方案似乎是正确的,但他们没有提及它的名称Currying,这是针对类似情况的功能编程设计模式。比绑定速度快3到10倍,具体取决于浏览器。

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

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

查看不同浏览器中的性能提升


@TinyGiant具有返回函数的示例仍在进行性能优化。我不会像所有JavaScript博客一样跳入箭头函数的潮流。它们看上去很酷很干净,但是却促进了内联的编写函数,而不是使用预定义的函数。在炎热的地方这可能是一个不明显的陷阱。另一个问题是它们不仅是语法糖,因为它们执行不必要的绑定,从而创建了包装闭包。
Pawel

2
对未来读者的警告:这个答案错误地使用了Currying一词。“ Currying是当您将一个包含多个参数的函数分解为一系列包含一部分参数的函数时。” 。此代码不执行任何操作。您在这里所做的所有事情都是从已接受的答案中获取代码,四处移动,更改样式并命名,然后将其称为currying,而分类不是。

3

您的代码无效,因为它的作用是:

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

现在的问题是,i调用函数时变量的值是多少?由于第一个循环是使用条件创建的i < 3,因此当条件为false时,它将立即停止i = 3

您需要了解,在创建函数时,不会执行任何代码,只会将其保存以供以后使用。因此,当稍后调用它们时,解释器将执行它们并询问:“的当​​前值是i多少?”

因此,您的目标是先保存ito函数的值,然后再将函数保存到funcs。例如,可以通过以下方式完成此操作:

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

这样,每个函数将具有其自己的变量x,我们将其设置xi每次迭代中的值。

这只是解决此问题的多种方法之一。


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

3

使用let(blocked-scope)代替var。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.