一道IIFE经典面试题

112 阅读2分钟

题目是这样的:假设想通过循环+ 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以后新增了letconst,且改以{ }作为它的块级作用域。

换句话说,将上例中的for改为let就可以做到保留i在执行循环当下的「值」,打出一样的效果:

for( let i = 0; i < 5; i++ ) {
  window.setTimeout(function() {
    console.log(i);
  }, 1000*(i+1));
}

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再那么必要了。