关于for循环中的闭包问题,其实早在之前我学习闭包的时候,就看过不下两篇文章提到过了,算是一个很经典的问题了。
尴尬的是,当同事叫我帮忙看这个问题的时候,我虽然一眼就看出了问题产生的原因,但是却死活想不起来解决方法,没办法只好又去google了一下才解决。
所以今天额外写一篇采坑记录加深印象
问题描述
问题产生的原因就是在for循环中使用了闭包,导致了“出乎意料”的结果。
代码演示:
var funcArr = [];
for(var i=1;i<=10;++i){
var func = function(){
console.log("这是第 "+i+" 个Function");
}
funcArr.push(func);
}
funcArr.forEach(function(func){
func();
});
像上面这一份代码,目的是创建10个函数,每个函数都打印自己生成时的序号。
那么在写这一份代码的时候,心里肯定已经有这样的预期输出了:
这是第 1 个Function
这是第 2 个Function
这是第 3 个Function
...
这是第 10 个Function
但是结果却输出的是10个完全一样的如下文字:
这是第 11 个Function
为什么呢?
问题原因
之所以会造成上面这样的输出,是因为代码在for循环中创建了内部函数,创建的内部函数使用了外部函数的变量 i ,也就是说生成的10个函数都拥有对同一个变量 i 的引用(而不是在创建函数时i的值)。
这里除了闭包外,还涉及到一个知识点:js的变量作用域,由于js没有块级作用域,因此变量 i 在for循环体结束后仍然存在。
关于闭包和作用域这两个概念不清楚的话,可以查看我写的这两篇文章:javascript与块级作用域,深入理解闭包。
在循环结束后,i 的值自增到了11,此时调用这10个使用了i值的函数,就会打印最新的i值,所以最后输出了10个 “这是第 11 个Function”。
解决方法
首先是一个错误的解决方法:
知道了问题原因是10个函数拥有的是i的引用而不是被创建时i的值后,就会想当然地用一个临时变量保存i的副本,然后每个函数使用副本替代i。
于是信手写了下面的代码:( 既然我都说了这是一个错误的方法,你可以先尝试自己想想错在哪里 )
var funcArr = [];
for(var i=1;i<=10;++i){
var temp = i;//保存i的副本
var func = function(){
console.log("这是第 "+temp+" 个Function");
}
funcArr.push(func);
}
funcArr.forEach(function(func){
func();
});
这份代码的输出结果是10个一模一样的,如下面的文字:
这是第 10 个Function
你会发现这个结果和之前的代码唯一区别在于输出的是10而不是11。
为什么呢?
其实还是因为js没有块级作用域的原因,变量temp虽然是在for循环体中定义的,但是在for循环结束后仍然存在,生成的10个函数仅仅是从原来的引用i变成了引用temp,而temp最后的值是for循环最后一趟循环时i的值,也就是10,所以产生了上的输出结果。
正确的解决方法:
要解决这个闭包产生的问题需要“以毒攻毒”,再用一个闭包来保存创建函数时i的值;
代码如下:
var funcArr = [];
for(var i=1;i<=10;++i){
var func = (function(n){
return function(){
console.log("这是第 "+n+" 个Function")
};
})(i)
funcArr.push(func);
}
funcArr.forEach(function(func){
func();
});
这份代码创建了一个立即执行的匿名函数,并将i的值作为参数n传给这个匿名函数。
由于这个匿名函数返回一个内部函数,内部函数引用了匿名函数的参数值n,因此会形成一个闭包,闭包使得匿名n的值得以保存在内存中,每一趟循环都会创建一个新的匿名函数,新的匿名函数又将形成新的闭包,而且匿名函数的参数值随着i值的变化而变化,因此可以实现预期的输出结果。