for循环+setTimeout遇到的问题

868 阅读1分钟

我正在参与掘金新人创作活动,一起开启写作之路。

关键词:作用域,闭包,延时函数

以下代码会打印5次5,而不会依次打印0~4:

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

image.png

原因:通过var声明的变量会发生变量提升。即等价于以下代码:

var i; // i为全局变量
for (i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

// 或者下面这样展示更直观
var i; // i为全局变量
for (i = 0; i < 5; i++) { }
// i==5了
setTimeout(() => {
  console.log(i) // 5
}, 1000)
setTimeout(() => {
  console.log(i) // 5
}, 1000)
setTimeout(() => {
  console.log(i) // 5
}, 1000)
setTimeout(() => {
  console.log(i) // 5
}, 1000)
setTimeout(() => {
  console.log(i) // 5
}, 1000)

静态代码执行完时(i=5时跳出循环),全局变量i就已经变为5了。1s之后,由于setTimeout内的变量i来自全局作用域,因此执行setTimeout时,传入的是全局变量i的最终值:5。效果就是打印5次5。

解决方法:

方法1:给setTimeout封装到一个局部函数,每个局部函数都拥有一个独立的作用域,内部的变量会遮蔽全局作用域的同名变量,也不会受其他代码块的影响,俗称闭包

for (var i = 0; i < 5; i++) {
  (
    // 形参m由i赋值
    function (m) {
      setTimeout(() => {
        console.log(m)
      }, 1000);
    }
  )(i)
}

方法2:使用let或const关键词

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

方式3:块级作用域(和方法2一样)

for (var i = 0; i < 5; i++) {
  {
    // {}内添加一个let或const后,会成为块作用域
    let j = i;
    setTimeout(() => {
      console.log(j);
    }, 1000);
  }
}

以上内容含自己的理解,仅用于自学记录。