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在执行期间解析变量的方式。

每个函数定义形成一个范围,该范围由所有本地var声明的变量及其参数。如果我们在另一个(外部)函数中定义了内部函数形成一条链,并将在执行过程中使用当函数被执行时,运行时通过搜索作用域链来评估变量。如果在链的某个点上可以找到一个变量,它将停止搜索并使用它,否则将继续搜索,直到到达属于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时,作用域链将是函数inner->global。由于在函数内部找不到变量i(既没有使用var声明,也没有作为参数传递),因此它继续搜索,直到最终在全局范围(即window.i)中找到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时,现在作用域链将是函数inner->函数outer。这一次,可以在for循环中执行3次的外部函数的作用域中找到i,每次都有正确绑定的值i。当内部执行时,它不会使用window.i的值。

更多详情请点击此处它包括在循环中创建闭包的常见错误,以及我们需要闭包的原因和性能考虑。

其他回答

最初的示例不起作用的原因是,您在循环中创建的所有闭包都引用了同一帧。实际上,一个对象上有3个方法,只有一个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</按钮>

最简单的解决方案是,

而不是使用:

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值都不同。

此解决方案似乎适合任何此类问题,因为它需要对受此问题困扰的原始代码进行最小的更改。事实上,这是故意的,根本不应该是一个问题!

聚会的时间有点晚了,但我今天正在探讨这个问题,并注意到许多答案并没有完全解决Javascript如何处理作用域的问题,这本质上就是问题的根源。

所以正如许多其他人提到的,问题是内部函数引用的是同一个i变量。那么为什么我们不在每次迭代时都创建一个新的局部变量,并让内部函数引用它呢?

//覆盖console.log(),以便可以看到控制台输出console.log=函数(消息){document.body.innerHTML+='<p>'+msg+'</p>';};var funcs={};对于(变量i=0;i<3;i++){var ilocal=i//创建新的局部变量funcs[i]=函数(){console.log(“我的值:”+ilocal)//每个都应该引用自己的局部变量};}对于(变量j=0;j<3;j++){函数[j]();}

就像之前一样,每个内部函数输出分配给i的最后一个值,现在每个内部函数只输出分配给ilocal的最后一值。但每个迭代不应该有自己的ilocal吗?

事实证明,这就是问题所在。每个迭代都共享相同的范围,因此第一次迭代之后的每个迭代都只是覆盖ilocal。来自MDN:

重要提示:JavaScript没有块范围。随块引入的变量的作用域是包含函数或脚本,设置这些变量的效果将持续到块本身之外。换句话说,块语句不引入范围。尽管“独立”块是有效的语法,但您不希望在JavaScript中使用独立块,因为如果您认为它们在C或Java中做类似的事情,它们不会做您认为它们做的事情。

再次强调:

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

通过在每次迭代中声明ilocal之前检查ilocal,我们可以看到这一点:

//覆盖console.log(),以便可以看到控制台输出console.log=函数(消息){document.body.innerHTML+='<p>'+msg+'</p>';};var funcs={};对于(变量i=0;i<3;i++){console.log(ilocal);var ilocal=i;}

这正是这个bug如此棘手的原因。即使您正在重新定义变量,Javascript也不会抛出错误,JSLint甚至不会抛出警告。这也是为什么解决这一问题的最佳方法是利用闭包,这本质上就是在Javascript中,内部函数可以访问外部变量,因为内部作用域“包围”外部作用域。

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

//覆盖console.log(),以便可以看到控制台输出console.log=函数(消息){document.body.innerHTML+='<p>'+msg+'</p>';};var funcs={};对于(变量i=0;i<3;i++){funcs[i]=(function(){//使用包装函数创建新范围var ilocal=i//将i捕获到本地var中return function(){//返回内部函数console.log(“我的值:”+ilocal);};})(); //记住运行包装器函数}对于(变量j=0;j<3;j++){函数[j]();}

在包装器函数内创建内部函数为内部函数提供了一个只有它才能访问的私有环境,即“闭包”。因此,每次调用包装器函数时,我们都会创建一个新的内部函数,并使用它自己的独立环境,确保ilocal变量不会相互冲突和覆盖。一些小的优化给出了许多其他SO用户给出的最终答案:

//覆盖console.log(),以便可以看到控制台输出console.log=函数(消息){document.body.innerHTML+='<p>'+msg+'</p>';};var funcs={};对于(变量i=0;i<3;i++){funcs[i]=包装器(i);}对于(变量j=0;j<3;j++){函数[j]();}//为内部函数创建单独的环境函数包装器(ilocal){return function(){//返回内部函数console.log(“我的值:”+ilocal);};}

使现代化

随着ES6成为主流,我们现在可以使用新的let关键字来创建块范围的变量:

//覆盖console.log(),以便可以看到控制台输出console.log=函数(消息){document.body.innerHTML+='<p>'+msg+'</p>';};var funcs={};for(设i=0;i<3;i++){//使用“let”声明“i”funcs[i]=函数(){console.log(“我的值:”+i)//每个都应该引用自己的局部变量};}对于(var j=0;j<3;j++){//我们可以在这里使用“var”函数[j]();}

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

  asyncIterable = [1,2,3,4,5,6,7,8];

  (async function() {
       for await (let num of asyncIterable) {
         console.log(num);
       }
    })();