js经典闭包

setTimeout函数之循环和闭包

前言

之前对于setTimeout的一个经典问题的理解总是感到很迷惑,现在好像清晰一点了,所以把我的理解写下来,我对js的理解也不深入,如果有错误,请务必指出。以免误导其他看到这篇文章的小白。^-^.

先来点开胃菜

先看看这种很常见的问题吧:

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

上面这个例子来自《你不知道的JavaScript》,相信这种类似的问题也很常见,我最早见到这个例子是在TypeScript的文档里面,当时就不是很理解,对于输出的结果也就是强行记忆为“console.log(i)执行的时候i变为6了”,但对于这中间的大致流程却是十分模糊,以至于我当时 错误的以为 for循环和同步异步有什么关系。

正篇

先说下上面代码的运行结果:运行时会以 每秒一次 的频率输出 五次6 .

先抛开为什么结果是五次6这个问题,为什么这个频率会是每秒一次呢?可能大家刚开始的时候会有这种想法:“setTimeout函数的作用不是推迟执行里面的回调函数吗? 那结果就应该是for循环第一次时延迟一秒输出1,然后是for循环第二次,延迟两秒输出2然后以此类推 或者 到最后i的值为6所以应该是以6秒为周期循环打印6?

这里就遇到了第一个坑,对setTimeout函数理解有偏差。

为什么是每秒一次呢?

SF来帮忙

这是我在segmentfault上看到的一个问题。 原问题链接 。请参考第二个回答。

setTimeout的延迟不是绝对精确的;

setTimeout的意思是传递一个函数,延迟一段时候把该函数添加到队列当中,并不是立即执行;

所以说如果当前正在运行的代码没有运行完,即使延迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;也就是说所有传递给setTimeout的回调方法都会在整个环境下的所有代码运行完毕之后执行;

观察下面的代码:

setTimeout(function(){
        console.log("here");
    }, 0);
    var i = 0;
    //具体数值根据你的计算机CPU来决定,达到延迟效果就好
    while (i < 3000000000) {
        i ++;
    }
    console.log("test");

试着将上面的代码运行了遍下,结果为在过了一段时间之后,先打印了test,然后才是here。而且 需要注意的是
,上面的代码写的是setTimeout(..,0),如果按照之前 错误地
将setTimeout函数理解为延迟一段时间执行,那这里把时间赋为0岂不是马上执行了?而实验结论则印证了上面"setTimeout的意思是传递一个函数,延迟一段时间把该函数添加到队列中,并不是立即执行“的结论。(涉及到线程,异步,事件循环的知识我现在理解得还不到位,所以暂且不表)

现在再来想想为什么是每秒一次

再回到最初的那个问题,刚进入for循环的时候,i为1,所以相对于现在延迟一秒将timer函数添加到队列当中,然后for循环还要继续啊,并没有等一秒再继续循环啊,然后进行第二次循环,这时候i为2,所以相对于现在延迟两秒将timer函数送进队列。以此类推。for循环的时间忽略不计的话,timer函数就以每秒一次的频率执行啦。

为什么每次都显示6呢?

这个问题我 个人
觉得与异步和闭包都有关系。

首先和异步的关系上文已经说了。

和闭包的关系

先要清楚,什么是闭包?过去我也把闭包和立即执行函数 错误的
混为一谈,看着立即执行函数表达式的括号我就 天真地
以为:用括号把函数包裹起来,这不就是”闭“包吗?

《你不知道的JavaScript》书中,对闭包的解释大概是这样的:对函数类型的值进行传递时,保留对它 被声明的位置所处的作用域
的引用。

也许上面这句话我总结得比较晦涩,但原书对这个问题解释得要清晰一些,可以看看原书47页。

那timer函数是在setTimeout函数中被声明的吧?在执行timer函数中的console.log(i)的时候,这个i是多少呢?在timer函数中没有i的声明啊。那就继续向外层的作用域找,终于在全局作用域下找到了i为多少了。

var的疑问

再来看看那个for循环, for(var i = 1; i <= 5;="" i++){...}
,在这里其实隐含着函数作用域和块作用域的的陷阱。在这段代码中用var声明的变量i的作用域在哪呢?是在当前作用域还是{}所包裹的内部呢?其实我们只要明确刚才这段代码相当于下面的代码就清除i的作用域在哪了。

var i;
for(i = 1; i <= 5;="" i++)
  

这就是每次的输出都是6的原因

所以,当timer函数第一次执行的时候,在执行console.log(i)的时候,这个时候的i其实是全局作用域下的i,这个时候循环是已经结束了,这时候i为6.(再次提醒 不要错误地认为 要等timer函数执行之后才会继续循环,再看看什么是异步);

那么问题来了

那么,怎么改动上面的代码让结果依次为1,2,3,4,5呢?最简单的办法就是将var改为let,原因是let创建了块作用域。(具体是怎么回事暂且不表,可以用babel将ES6转换为ES5查看结果。但是原理和下面要讲的类似)

所以,再想想为什么会每次的输出都是6呢?是因为每次执行到console.log(i)的时候这个i是全局作用域下的i啊,那怎么才能让这个i为每次循环时的i呢?即 怎么才能在每次循环时”捕获“到i的副本呢

不要急,先来看看为什么可以用立即执行函数表达式。

所以下面的代码有用吗?

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

    

上面这个例子同样是来自《你不知道的JavaScript》。我以前 错误地 认为,立即执行函数表达式,这是立即执行啊,所以里面的timer也立即执行了,所以就能输出1,2,3,4,5了。

先说答案,这样当然是 不行 的,这里的立即执行也只是立即执行了setTimeout函数,而setTimeout函数的作用也就是将timer函数延迟一段时间添加到队列,所以这个立即执行表达式在这里有没有都一样。我之前错误的想法也是受到了”立即执行“这四个字的误导。先来看看一个正确答案:

for (var i = 1; i <= 5;="" i++)="" {="" (function()="" var="" j="i;" settimeout(="" function="" timer()="" console.log(j);="" },i*1000="" );="" 这一行将i*1000改为j*1000也行,并不影响="" })();="" }

    

发现这个答案和上面的错误答案的区别了吗?其实我们是 用立即执行函数表达式创造了新的函数作用域将timer函数包裹了起来,并用j捕获了每次循环时的i ,这样在运行到console.log(j)的时候显示的就是每次循环时的i值啦。

同理还有这样的写法:

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

还有一些其他写法这里就不一一列举了,原理都是和作用域相关。其实上面这个涉及到let的例子和块作用域相关,这里就不展开了。

总结

异步决定了这段代码打印i的频率,闭包和作用域的知识决定了这个i是多少以及怎样改写这段代码。

总觉得这篇文章还有一些欠缺,希望大家能指正。

博客园-原创精华区稿源:博客园-原创精华区 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 前端开发 » js经典闭包

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录