在上文 前端基础进阶(五):闭包 中的结尾留下了一个关于setTimeout与循环闭包的思考题。

利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

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

setTimeout 方法会设置一个计时器,一旦计时器到期,该计时器就会执行一个函数或指定的一段代码。
它的返回timeoutID的是一个正整数值,用于标识调用创建的计时器setTimeout()。
该函数详细介绍请移步: setTimeout - MDN

1
2
3
4
5
6
7
8
setTimeout(code)
setTimeout(code, delay)

setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* ... ,*/ paramN)
参数说明
functionRefAfunction在计时器到期后执行。
code允许您包含字符串而不是函数的替代语法,该语法在计时器到期时编译并执行。由于与使用安全风险,不建议使用eval()语法。
delay 可选在执行指定的函数或代码之前计时器应等待的时间(以毫秒为单位)。如果省略此参数,则使用值 0,表示“立即”执行,或者更准确地说,执行下一个事件循环。
param1, …,paramN 可选传递给指定函数的附加参数 function。

执行结果如图:

上图中的数字7,就是这个唯一的timeoutID。在使用时,常常会使用一个变量将这个唯一的timeoutID保存起来,用以传入clearTimeout,清除定时器。

接下来,我们还需要考虑另外一个重要的问题,那就是setTimeout中定义的操作,在什么时候执行?为了引起大家的重视,我们来看看下面的例子。

1
2
3
4
5
var timer = setTimeout(function() {
console.log('setTimeout actions.');
}, 0);

console.log('other actions.');

思考一下,当我将setTimeout的延迟时间设置为0时,上面的执行顺序会是什么?

在浏览器中的console中运行试试看,很容易就能够知道答案。

在对于 执行上下文 的介绍中,与大家分享了 函数调用栈 这种特殊数据结构的调用特性。在这里,将会介绍另外一个特殊的队列结构,页面中所有由setTimeout定义的操作,都将放在同一个队列中依次执行。

队列数据结构的特点:先进先出
队列数据结构的特点:先进先出

而这个队列执行的时间,需要等待到函数调用栈清空之后才开始执行。即所有可执行代码执行完毕之后,才会开始执行由setTimeout定义的操作。而这些操作进入队列的顺序,则由设定的延迟时间来决定。

因此在上面这个例子中,即使我们将延迟时间设置为0,它定义的操作仍然需要等待所有代码执行完毕之后才开始执行。这里的延迟时间,并非相对于setTimeout执行这一刻,而是相对于其他代码执行完毕这一刻。

为了帮助大家理解,再来一个结合变量提升的更加复杂的例子。如果你能够正确看出执行顺序,那么你对于函数的执行就有了比较正确的认识了,如果还不能,就回过头去看看其他几篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();

执行结果如图所示:

到这一步,关于setTimeout就暂时先介绍到这里,我们回过头来看看循环闭包的思考题。

1
2
3
4
5
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值并保存在闭包中。

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
利用断点调试,在chrome中查看执行顺序与每一个闭包中不同的i值
利用断点调试,在chrome中查看执行顺序与每一个闭包中不同的i值

当然,也可以在setTimeout的第一个参数处利用闭包。

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
setTimeout((function (i) {
return function () {
console.log(i);
}
})(i), i * 1000);
}

闭包之外的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// setTimeout第三个参数
for (var i=1; i<=5; i++) {
setTimeout( function timer(i) {
console.log(i);
}, i*1000,i );
}
// bind
for (var i=1; i<=5; i++) {
setTimeout( function timer(i) {
console.log(i);
}.bind(null,i), i*1000 );
}
// let
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}