想要说明闭包,for循环是最常见的例子:

for(var i=1;i<=5;i++)
{
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}

以我们所想,我们可能认为他会输出1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次6。

这是为什么?

原因是延迟函数会在循环结束时才执行,事实上,当定时器运行时即使每个迭代中执行的是setTimeout(...,0),所有的回调函数依然是在循环结束后才会执行,因此会每次输出一个6出来。

根据作用域的原理,实际情况:尽管循环中的五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
所以所有函数共享一个i的引用时,循环结构让我们误认为背后还有更复杂的机制在器作用,但实际上啥都木有,如果将延迟函数的回调重复定义五次,完全不使用循环,那他同这段代码是完全等价的。

解决方法如下:
我们先试一下:

    for(var i=1;i<5;i++){
        (function(){
            setTimeout(function timer(){
                console.log(i);
            },i*1000);
        })();
    }

看似可以,但实际也没用,虽然这样写我们有更多词法作用域了,的确每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来。
如果作用域是空的,那么仅仅将他们进行封闭是不够的。仔细看一下,我们的IIFE只是一个什么都没有的空作用域,所以需要包含一点实际内容为我们所用。

他需要自己的变量,用来在每个迭代中存储i的值:

    for(var i=0;i<=5;i++)
    {
        (function(){
            var j=i;
            setTimeout(function timer(){
                console.log(j);
            },j*1000);
        })();
    }

ok,他运行如我们所愿了!

可以进行改进:

    for(var i=1;i<=5;i++)
    {
        (function{
            setTimeout(function timer(){
                console.log(j);
            },j*1000);
        })(i);    //i可以改动,只要你喜欢
    }

在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会包含一个具有正确值的变量供我们访问。

使用let解决

for循环的let声明还会有一个特殊行为,这个行为之处变量在循环过程中不知被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

    for(var i=1;i<=5;i++)
    {
        let j=i;  //闭包
        setTimeout(function timer(){
            console.log(j);
        },j*1000);
    }
    
    下面是进化版
    
    for(let i;i<=5;i++)
    {
        setTimeout(function timer(){
            console.log(i);
        },i*1000);
    }

战五渣
12 声望15 粉丝

不管你理或不理,bug就在那里


« 上一篇
闭包