JavaScript 的异步执行机制和闭包特性

for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log(i);
  }, 1000);
}
// 输出:5 5 5 5 5(1秒后同时打印)具体发生了什么
这段代码涉及 JavaScript 的异步执行机制和闭包特性,导致输出结果与直觉不符。具体解释如下:

执行流程拆解

  1. 循环同步执行:
    JavaScript 先同步执行 for 循环,由于 var 声明的变量 i 属于函数作用域(而非块级作用域),循环内的 setTimeout 虽然被依次调用,但其中的回调函数并未立即执行。
  2. 定时器异步执行:
    setTimeout 的回调函数被放入任务队列,等待主线程执行完毕后才会触发。此时,for 循环早已结束,i 的值已经变为 5(循环条件 i<5 不满足时退出,此时 i=5)。
  3. 闭包捕获变量:
    每个回调函数通过闭包捕获的是同一个变量 i(而非循环时的值)。当回调函数最终执行时,它们读取的 i 已固定为 5,因此全部打印 5

为什么会 “同时打印”?

所有 setTimeout 的延迟时间均为 1000ms,且是在循环中连续同步设置的。因此,它们会在循环结束约 1 秒后几乎同时从任务队列中被取出执行,导致 5 个 5 看起来是 “一起打印” 的。

如何修复?

要让输出符合预期(依次打印 0 1 2 3 4),需确保每个回调函数捕获的是当前循环迭代的值。以下是两种常见方法:

方法 1:使用 let(块级作用域)

for(let i=0; i<5; i++){
  setTimeout(function(){
    console.log(i);  // 正确输出:0 1 2 3 4(每秒一个)
  }, 1000);
}
原理:let 声明的变量具有块级作用域,每次循环会创建一个新的 i 副本,闭包捕获的是当前迭代的 i

方法 2:使用立即执行函数(IIFE)

for(var i=0; i<5; i++){
  (function(j){
    setTimeout(function(){
      console.log(j);  // 正确输出:0 1 2 3 4
    }, 1000);
  })(i);  // 通过参数传递当前i的值
}
原理:利用立即执行函数创建独立闭包,将当前 i 的值传递给内部变量 j,避免所有回调共享同一个 i

总结

  • 闭包捕获变量:闭包保存的是变量的引用,而非值的副本。
  • 异步执行时机:setTimeout 的回调会在循环结束后执行。
  • 作用域差异:var 是函数作用域,let 是块级作用域。
posted @ 2022-05-31 17:36  我歌且谣  阅读(43)  评论(0)    收藏  举报