理解for循环中的异步取值问题—五道题就够了

复习:js单线程、事件循环、任务队列、执行顺序、异步、作用域、闭包

一、var声明变量
(1)栗子1
for (var t1=0;t1<4;t1++){
   setTimeout(function(){console.log(t1)}, 1000)
}
//4 4 4 4

java for循环内使用异步执行 部分不执行 for循环内异步问题_前端

解析:js是单线程的,js分为同步任务和异步任务,同步任务在主线程上进行,异步任务在异步队列里面不进入主线程。定时器排队在异步任务队列的队尾。当定时器时间到了且主线程空闲的时候会去异步任务队列调用任务进入主线程。

为什么输入是4,4,4,4,不是0,1,2,3呢?

for循环圆括号()里面的代码是同步代码,同步代码执行完毕后,for循环里面 i 的值已经变成4,循环已经结束。因为setTimeout的 i 值是全局变量,for形成一个块级作用域,全局变量可以跨块级作用域,每次循环这个值都会被改变,指向的是最外层全局变量 i 的值。1s打印的时候,i 已经变成4了。

(2)栗子2

有人说,用立即执行函数就可以避免回调。如果匿名立即执行函数呢?结果是怎么样的,我们用匿名立即执行函数试试看。

立即执行函数就是字面意思,代码加载后立即执行,无需等待回调。

//匿名立即执行函数不传入形参
for (var t1=0;t1<4;t1++){
   (function(){
      setTimeout(()=>{console.log(t1)}, 1000)
   })(t1)
}
// 4 4 4 4

java for循环内使用异步执行 部分不执行 for循环内异步问题_立即执行函数_02

解析:为什么输出还是4,4,4,4,不是0,1,2,3呢?明明用了立即执行函数了!这就涉及到闭包。

匿名立即执行函数没有传入形参,传入实参t1。在立即执行函数创建的作用域里面没有找到t1,会往外查找,就找到了t1,(闭包)这时候t1是var声明的全局变量,for同步代码执行后t1的值早已经为4。四个定时器里面的匿名函数共享了同一作用域里面的同一个变量,所以打印4,4 ,4 ,4。

(3)栗子3
//匿名立即执行函数传入形参
for (var t1=0;t1<4;t1++){
   (function(t1){
      setTimeout(()=>{console.log(t1)}, 1000)
   })(t1)
}
// 0 1 2 3

java for循环内使用异步执行 部分不执行 for循环内异步问题_块级作用域_03

解析:为什么传入形参之后就可以打印出0,1,2,3了呢?

匿名立即执行函数传入形参,每个t1的值都会传入function声明的匿名立即执行函数,利用闭包使得函数可以继续访问定义时的作用域,每次循环的时候,新生成的作用域将每一次循环的 t1 值保存下来,所以打印出0,1,2,3。

二、let声明变量

let是ES6新增的声明变量,let可以声明一个独立的块级作用域。

(1)栗子1
for (let t1=0;t1<4;t1++){
   setTimeout(function(){console.log(t1)}, 1000)
}
// 0 1 2 3

java for循环内使用异步执行 部分不执行 for循环内异步问题_块级作用域_04

解析:var换成let声明之后,为什么输出0,1,2,3?

let声明一个独立的块级作用域,在每个块级作用域里面的 t1 都是属于该作用域的,即每个作用域都有它自己的 t1。在for循环中用let来定义循环变量,每一次循环都会重新声明变量t1,随后的每个循环都会使用上一个循环结束时的值来初始化这个变量t1。

(2)栗子2
for (let t1=0;t1<4;t1++){
   (function(e) {
     setTimeout(function(){console.log(e)}, 1000)
   })(t1);
}
// 0 1 2 3