var函数=[];//让我们创建3个函数对于(变量i=0;i<3;i++){//并将其存储在funcs中funcs[i]=函数(){//每个都应该记录其值。console.log(“我的值:”,i);};}对于(变量j=0;j<3;j++){//现在让我们逐一查看函数[j]();}

它输出以下内容:

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

而我希望它输出:

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


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

var buttons=document.getElementsByTagName(“button”);//让我们创建3个函数for(var i=0;i<buttons.length;i++){//作为事件侦听器buttons[i].addEventListener(“单击”,函数(){//每个都应该记录其值。console.log(“我的值:”,i);});}<按钮>0</按钮><br/><button>1</button><br/><按钮>2</按钮>

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

//一些异步等待函数const wait=(ms)=>new Promise((解析,拒绝)=>setTimeout(解析,ms));对于(变量i=0;i<3;i++){//一旦每个承诺得到解决,就记录“i”。等待(i*100)。然后(()=>console.log(i));}

在for in和for of循环中也很明显:

常量arr=[1,2,3];常量fns=[];用于(arr中的变量i){fns.push(()=>console.log(“index:”,i));}对于(arr的var v){fns.push(()=>console.log(“值:”,v));}for(arr常量){var obj={number:n};//或新的MyLibObject({…})fns.push(()=>console.log(“n:”,n,“|”,“obj:”,JSON.stringify(obj)));}for(fns的var f){f();}

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


当前回答

您需要了解的是javascript中变量的范围是基于函数的。这与c#有一个重要的区别,因为c#有块作用域,只需将变量复制到for中的一个即可。

将其包装在一个函数中,该函数会像apphacker的答案那样计算返回函数的值,这会起到作用,因为变量现在具有函数作用域。

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

var funcs={};对于(变量i=0;i<3;i++){设索引=i//添加此funcs[i]=函数(){console.log(“我的值:”+索引)//更改副本};}对于(变量j=0;j<3;j++){函数[j]();}

其他回答

还有另一个解决方案:不要创建另一个循环,只需将this绑定到返回函数。

var函数=[];函数createFunc(i){返回函数(){console.log('我的值:'+i)//i的对数值。}.调用(this);}对于(var i=1;i<=5;i++){//5函数funcs[i]=创建Func(i);//调用createFunc()i=5次}

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

这是异步代码中经常遇到的问题,变量i是可变的,在调用函数时,使用i的代码将被执行,并且i将变为其最后一个值,因此意味着在循环中创建的所有函数都将创建一个闭包,i将等于3(for循环的上限+1)。

解决此问题的一个方法是创建一个函数,该函数将为每次迭代保存i的值,并强制复制i(因为它是一个原语,如果有帮助,请将其视为快照)。

这个问题确实展示了JavaScript的历史!现在,我们可以避免使用箭头函数的块作用域,并使用Object方法直接处理来自DOM节点的循环。

常量函数=[1,2,3].map(i=>()=>console.log(i));函数映射(fn=>fn())

const buttons=document.getElementsByTagName(“button”);对象按键(按钮).map(i=>按钮[i].addEventListener('click',()=>console.log(i)));<button>0</button><br><button>1</button><br><按钮>2</按钮>

这描述了在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的新副本也被创建以引用新对象。因此,计数器1和计数器2彼此独立。

回路中的闭合

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

考虑:

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

请注意,计数器[0]和计数器[1]不是独立的。事实上,它们对同一个对象进行操作!

这是因为在循环的所有迭代中只有一个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);
  }
}

这是因为直接分配了函数范围中的局部变量以及函数参数变量新副本。

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

var函数=[];对于(设i=0;i<3;i++){funcs[i]=函数(){console.log(“我的值:”+i);};}

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

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

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

此处的文档:

常量允许

不过,请注意,IE9-IE11和Edge 14之前的Edge支持以上错误(它们不会每次都创建一个新的i,所以上面的所有函数都会记录3,就像我们使用var时一样)。边缘14最终得到了正确的结果。