关于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值的变化而变化,因此可以实现预期的输出结果。

发表评论

电子邮件地址不会被公开。 必填项已用*标注