前端基础进阶(六):setTimeout与循环闭包面试题详解
在上文 前端基础进阶(五):闭包 中的结尾留下了一个关于setTimeout与循环闭包的思考题。
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
1 | for (var i = 1; i <= 5; i++) { |
setTimeout 方法会设置一个计时器,一旦计时器到期,该计时器就会执行一个函数或指定的一段代码。
它的返回timeoutID的是一个正整数值,用于标识调用创建的计时器setTimeout()。
该函数详细介绍请移步: setTimeout - MDN
1 | setTimeout(code) |
参数 | 说明 |
---|---|
functionRef | Afunction在计时器到期后执行。 |
code | 允许您包含字符串而不是函数的替代语法,该语法在计时器到期时编译并执行。由于与使用安全风险,不建议使用eval()语法。 |
delay 可选 | 在执行指定的函数或代码之前计时器应等待的时间(以毫秒为单位)。如果省略此参数,则使用值 0,表示“立即”执行,或者更准确地说,执行下一个事件循环。 |
param1, …,paramN 可选 | 传递给指定函数的附加参数 function。 |
执行结果如图:
上图中的数字7,就是这个唯一的timeoutID。在使用时,常常会使用一个变量将这个唯一的timeoutID保存起来,用以传入clearTimeout,清除定时器。
接下来,我们还需要考虑另外一个重要的问题,那就是setTimeout中定义的操作,在什么时候执行?为了引起大家的重视,我们来看看下面的例子。
1 | var timer = setTimeout(function() { |
思考一下,当我将setTimeout的延迟时间设置为0时,上面的执行顺序会是什么?
在浏览器中的console中运行试试看,很容易就能够知道答案。
在对于 执行上下文 的介绍中,与大家分享了 函数调用栈 这种特殊数据结构的调用特性。在这里,将会介绍另外一个特殊的队列结构,页面中所有由setTimeout定义的操作,都将放在同一个队列中依次执行。
而这个队列执行的时间,需要等待到函数调用栈清空之后才开始执行。即所有可执行代码执行完毕之后,才会开始执行由setTimeout定义的操作。而这些操作进入队列的顺序,则由设定的延迟时间来决定。
因此在上面这个例子中,即使我们将延迟时间设置为0,它定义的操作仍然需要等待所有代码执行完毕之后才开始执行。这里的延迟时间,并非相对于setTimeout执行这一刻,而是相对于其他代码执行完毕这一刻。
为了帮助大家理解,再来一个结合变量提升的更加复杂的例子。如果你能够正确看出执行顺序,那么你对于函数的执行就有了比较正确的认识了,如果还不能,就回过头去看看其他几篇文章。
1 | setTimeout(function () { |
执行结果如图所示:
到这一步,关于setTimeout就暂时先介绍到这里,我们回过头来看看循环闭包的思考题。
1 | for (var i = 1; i <= 5; i++) { |
如果直接这样写,根据setTimeout定义的操作在函数调用栈清空之后才会执行的特点,for循环里定义了5个setTimeout操作。而当这些操作开始执行时,for循环的i值,已经先一步变成了6。因此输出结果总为6。想要让输出结果依次执行,就必须借助闭包的特性,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值即可。
如果知道在函数中闭包判定的准则,即执行时是否在内部定义的函数中访问了上层作用域的变量。我们则需要包裹一层自执行函数为闭包的形成提供条件。
因此,只需要2个操作就可以完成题目需求,一是使用自执行函数提供闭包条件,二是传入i值并保存在闭包中。
1 | for (var i = 1; i <= 5; i++) { |
当然,也可以在setTimeout的第一个参数处利用闭包。
1 | for (var i = 1; i <= 5; i++) { |
闭包之外的方法:
1 | // setTimeout第三个参数 |