for循环里的定时器

先看一段代码。

  1. let i = 0
  2. for(i = 0; i<6; i++){
  3. setTimeout(()=>{
  4. console.log(i)
  5. },0)
  6. }

熟悉这道题目的人立马可以说出答案:6个6~
但是很多新手第一次看到这段代码时会产生一个错觉,认为打印结果会是0,1,2,3,4,5。
为什么明明定时器的时间设置为了0,定时器却在console.log(‘a’)这句代码运行了之后才运行?
原来在 js 的世界里,定时器是一个异步任务。
这里有一个形象的比喻:
当你在打游戏时,妈妈喊你出去吃饭,你说:”马上就来”,你现在有两个选择:1. 关掉游戏,不管其他四个队友 2. 继续打团,结束游戏后立马去吃饭。这时,你手头上的游戏就像一系列同步任务,妈妈的催促就像定时器。
而 JS 会优先执行当前的同步任务,在同步代码执行结束后才会去处理任务队列中的异步任务。
这样,即便定时器设置了0,也是在忙完手头的事情之后才会去读取任务队列。
因此在所有同步代码执行完毕之后,for循环里的i值早已变成了6,循环已经结束。注意,for循环的圆括号部分也是同步代码。
这就是为什么打印出来6个6,而不是0,1,2,3,4,5。

总结一下,JS 里所有任务可以分成两种,一种是同步任务,另一种是异步任务。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

闭包

「函数」和「函数内部能访问到的变量」的总和,就是一个闭包。
v2-2d16967becf2df18358d62a84d0595e7_720w.jpg

let 关键字和立即执行函数

如果想实现for循环里的定时器打印出0,1,2,3,4,可以使用ES6的 let 关键字。

  1. for(let i = 0; i < 5; i++) {
  2. setTimeout(function () {
  3. console.log(i);
  4. });
  5. }

let 关键字劫持了 for 循环的块作用域,产生了类似闭包的效果。
在使用 let 声明变量 i 时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量,每个 setTimeout 引用的都是不同的变量实例,所以遍历出来的是符合新手期望的值,也就是循环执行过程中每个迭代变量的值。
除此之外,还可以使用立即执行函数。

立即执行函数

原理

ES 5 时代,为了得到局部变量,必须引入一个函数
但是这个函数如果有名字,就得不偿失,因为将全局变量放入一个函数变为局部变量,但这个函数是全局函数
于是这个函数必须是匿名函数
声明匿名函数,然后立即加个 () 执行它
但是 JS 标准认为这种语法不合法,所以 JS 程序员寻求各种办法
最终发现,只要在匿名函数前面加个运算符即可!、~、()、+、- 都可以
但是这里面有些运算符会往上走
所以推荐永远用 ! 来解决

格式

  1. ! function (){
  2. var a = 2
  3. console.log(a)
  4. } () //最后这个括号为引用该匿名函数

用立即执行函数完成题目

  1. for(var i = 0; i < 5; i++) {
  2. !function(i) {
  3. setTimeout(function () {
  4. console.log(i);
  5. });
  6. }(i)
  7. }

利用闭包的原理,闭包使一个函数可以继续访问它定义时的作用域。而这个新生成的作用域将每一次循环的当前i值单独保存了下来。