题目是这样的:假设想通过循环+ setTimeout来做到,在五秒钟之内,每秒钟依序通过console.log打印出:0 1 2 3 4
for( var i = 0; i < 5; i++ ) {
window.setTimeout(function() {
console.log(i);
}, 1000);
}
真的是这样吗?我们来看看执行的结果:
//过了接近一秒五个五同时打出
5
5
5
5
5
为什么会这样呢?
我们知道, JavaScript是一个「异步」的语言,所以当我们执行这段代码时,for循环并不会等待window.setTimeout结束后才继续,而是在执行阶段就一口气跑完。
也就是说,当window.setTimeout内的回调函数执行时,拿到的i已经是跑完for()循环的5。
那么要怎么解决这个问题呢?
我们可以把window.setTimeout包装成一个IIFE,这个问题就迎刃而解了:
for( var i = 0; i < 5; i++ ) {
// 为了凸显差异,我们将传入后的参数改名为 x
// 当然由于作用域的不同,要继续在内部沿用 i 也是可以的。
(function(x){
window.setTimeout(function() {
console.log(x);
}, 1000);
})(i);
}
这时候你会发现,执行的结果就会是我们预期的0 1 2 3 4了,但还是有一个问题:就是0 1 2 3 4还是在一秒钟后同时出现啊?怎么解决?
嘿嘿,相信聪明的你已经发现,由于for循环在一瞬间就跑完,等于那一瞬间它向window依序注册了五次timer,每个timer都只等待一秒钟,当然同时出现喽。
所以我们稍微修改一下:
for( var i = 0; i < 5; i++ ) {
(function(x){
// 将原来的 1000 改成 1000 * x
window.setTimeout(function() {
console.log(x);
}, 1000 * x);
})(i);
}
像这样,就可以依序打印出我们想要的结果喽!
[注] ES6以后新增了let与const,且改以{ }作为它的块级作用域。
换句话说,将上例中的for改为let就可以做到保留i在执行循环当下的「值」,打出一样的效果:
for( let i = 0; i < 5; i++ ) {
window.setTimeout(function() {
console.log(i);
}, 1000*(i+1));
}
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再那么必要了。