我在详细图解做用域链与闭包一文中的结尾留下了一个关于setTimeout与循环闭包的思考题。html
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5chrome
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
值得高兴的是不少朋友在读了文章以后确实对闭包有了更加深入的了解,并准确的给出了几种写法。一些朋友可以认真的阅读个人文章而且一个例子一个例子的上手练习,这种承认对我而言真的很是感动。可是也有一些基础稍差的朋友在阅读了以后,对于这题的理解仍然感到困惑,所以应一些读者老爷的要求,借此文章专门对setTimeout进行一个相关的知识分享,愿你们读完以后都可以有新的收获。浏览器
在最初学习setTimeout的时候,咱们很容易知道setTimeout有两个参数,第一个参数为一个函数,咱们经过该函数定义将要执行的操做。第二个参数为一个时间毫秒数,表示延迟执行的时间。数据结构
setTimeout(function() { console.log('一秒钟以后我将被打印出来') }, 1000)
可能很多人对于setTimeout的理解止步于此,但仍是有很多人发现了一些其余的东西,并在评论里提出了疑问。好比上图中的这个数字7,是什么?闭包
每个setTimeout在执行时,会返回一个惟一ID,上图中的数字7,就是这个惟一ID。咱们在使用时,经常会使用一个变量将这个惟一ID保存起来,用以传入clearTimeout,清除定时器。函数
var timer = setTimeout(function() { console.log('若是不清除我,我将会一秒以后出现。'); }, 1000) clearTimeout(timer); // 清除以后,经过setTimeout定义的操做并不会执行
接下来,咱们还须要考虑另一个重要的问题,那就是setTimeout中定义的操做,在何时执行?为了引发你们的重视,咱们来看看下面的例子。学习
var timer = setTimeout(function() { console.log('setTimeout actions.'); }, 0); console.log('other actions.'); // 思考一下,当我将setTimeout的延迟时间设置为0时,上面的执行顺序会是什么?
在浏览器中的console中运行试试看,很容易就可以知道答案,若是你没有猜中答案,那么我这篇文章就值得你点一个赞了,由于接下来我分享的小知识,可能会在笔试中救你一命。3d
在对于执行上下文的介绍中,我与你们分享了函数调用栈这种特殊数据结构的调用特性。在这里,将会介绍另一个特殊的队列结构,页面中全部由setTimeout定义的操做,都将放在同一个队列中依次执行。调试
我用下图跟你们展现一下队列数据结构的特色。code
而这个队列执行的时间,须要等待到函数调用栈清空以后才开始执行。即全部可执行代码执行完毕以后,才会开始执行由setTimeout定义的操做。而这些操做进入队列的顺序,则由设定的延迟时间来决定。
所以在上面这个例子中,即便咱们将延迟时间设置为0,它定义的操做仍然须要等待全部代码执行完毕以后才开始执行。这里的延迟时间,并不是相对于setTimeout执行这一刻,而是相对于其余代码执行完毕这一刻。因此上面的例子执行结果就很是容易理解了。
为了帮助你们理解,再来一个结合变量提高的更加复杂的例子。若是你可以正确看出执行顺序,那么你对于函数的执行就有了比较正确的认识了,若是还不能,就回过头去看看其余几篇文章。
setTimeout(function() { console.log(a); }, 0); var a = 10; console.log(b); console.log(fn); var b = 20; function fn() { setTimeout(function() { console.log('setTImeout 10ms.'); }, 10); } fn.toString = function() { return 30; } console.log(fn); setTimeout(function() { console.log('setTimeout 20ms.'); }, 20); fn();
OK,关于setTimeout就暂时先介绍到这里,咱们回过头来看看那个循环闭包的思考题。
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
若是咱们直接这样写,根据setTimeout定义的操做在函数调用栈清空以后才会执行的特色,for循环里定义了5个setTimeout操做。而当这些操做开始执行时,for循环的i值,已经先一步变成了6。所以输出结果总为6。而咱们想要让输出结果依次执行,咱们就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操做执行时,则访问对应闭包保存的i值便可。
而咱们知道在函数中闭包断定的准则,即执行时是否在内部定义的函数中访问了上层做用域的变量。所以咱们须要包裹一层自执行函数为闭包的造成提供条件。
所以,咱们只须要2个操做就能够完成题目需求,一是使用自执行函数提供闭包条件,二是传入i值并保存在闭包中。
for (var i=1; i<=5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i*1000 ); })(i) }
固然,也能够在setTimeout的第一个参数处利用闭包。
for (var i=1; i<=5; i++) { setTimeout( (function(i) { return function() { console.log(i); } })(i), i*1000 ); }