ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

闭包之循环中捕获正确的值

闭包之循环中捕获正确的值

先来看下面这几段 JavaScript 代码:它们执行后分别会输出什么?其中哪些能正常输出 0、1、2?

// 1、var 陷阱 for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // 2、 IIFE 闭包 for (var i = 0; i < 3; i++) { (function(index) { setTimeout(() => { console.log(index); }, 100); })(i); } // 3、 let 块级作用域 for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // 4、const + 传统 for for (const i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); } // 5、const + for...of for (const i of [0, 1, 2]) { setTimeout(() => { console.log(i); }, 100); }

答案↓


只有第 2、3、5 段能正常输出 0、1、2,你答对了吗?

第 1 段执行后输出 3、3、3;第 4 段则直接语法报错,只输出 0。为什么会这样呢?

从零到精通~~来理解吧

一、先看以下代码

var i; for (i = 0; i < 3; i++) { console.log(i); // ← 当场打印 } // 输出:0, 1, 2 ✅ // ----------------------------------------------------------------- // 块级作用域:let 和 const 只在 {} 内部有效 function blockScope() { { let blockVar = '我只在这里'; const blockConst = '我也只在这里'; console.log(blockVar); // ✅ 可以访问 } // console.log(blockVar); // ❌ 编译错误:找不到变量 } // 对比 var:没有块级作用域 function noBlockScope() { { var x = '我会逃逸出去'; } console.log(x); // ✅ 居然能访问!这就是问题所在 }

这个你应该很容易理解:同步版本会立即执行,因此依次输出 0、1、2。没有问题,每次打印时 i 恰好就是当时的值。

二、那代码改成异步后呢

var i; for (i = 0; i < 3; i++) { setTimeout(() => { console.log(i); // ← 过 3 秒才打印! }, 3000); } // 输出:3, 3, 3 ❌ // 把这里的var 改成 let for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 3000); } // 输出:0, 1, 2 ✅

那么问题来了:为什么用 let 就能正常打印,而用 var 却不行呢?

let 的情况: var 的情况: ┌───────────────┐ ┌───────────────┐ │ 块1: i = 0 │ │ │ │ setTimeout() │ │ var i │ └───────────────┘ │ (共用一个) │ ┌───────────────┐ │ │ │ 块2: i = 1 │ │ │ │ setTimeout() │ │ │ └───────────────┘ │ │ ┌──────────────- │ │ │ 块3: i = 2 │ │ │ │ setTimeout() │ │ │ └───────────────┘ └───────────────┘ 3 个独立的 i 只有 1 个 i

用一张图说明:let 在 for 循环里,JS 引擎自动给每一圈循环 套了一个独立的 {} 块 ,相当于每次循环都重新声明了一遍 i 。所以 3 个回调各看各的 i ,互不干扰。

而var 的i只有1个,3次循环都在改同一个i。

1、let循环执行时,js引擎实际干的事:

// 第 1 次循环 { let i = 0; setTimeout(() => console.log(i), 3000); } // 第 2 次循环 { let i = 1; setTimeout(() => console.log(i), 3000); } // 第 3 次循环 { let i = 2; setTimeout(() => console.log(i), 3000); }

2、var循环执行时,js引擎却是如下:

var i; // ← 只有一个,在循环外面 i = 0; setTimeout(() => console.log(i), 3000); i = 1; setTimeout(() => console.log(i), 3000); i = 2; setTimeout(() => console.log(i), 3000); i = 3; // 循环结束后 i 变成 3

3、那如果就是要用var呢,怎么修改代码才能打印出 0,1,2

是不是在执行的时候让函数立即执行(ImmediatelyInvokedFunctionExpression)就行了呢?那么就引入了IIFE 闭包(两个概念的组合)了。

(function() { // 代码... })(); // 注意这个 (),定义完立刻调用 // 解释 // fn() 执行fn函数 // 这里 fn 就是 function() {} // 也就是立即执行了 // 再看文章最开始的代码 // 2、 IIFE 闭包 for (var i = 0; i < 3; i++) { (function(index) { setTimeout(() => { console.log(index); }, 100); })(i); } // 理解了吧 立即执行fn(i) 就是执行 i是实参,index是形参 // 这里i立即传给函数了,所以 i是0 -》1 -》 2 function(index) { setTimeout(() => { console.log(index); }, 100); }

就像你写了个匿名函数,不等别人来调,当场就执行

闭包 = =函数等“记住”并访问它所在的作用域,即使函数在外面跑。

组合起来是什么?
用 IIFE 创建一个 独立的私有作用域 ,里面的函数引用了外部变量,就形成了 "IIFE 闭包"。

最经典的例子:

// 用 IIFE + 闭包实现计数器(不污染全局变量) const counter = (function() { let count = 0; // 私有变量,外面碰不到 return { increment() { count++; return count; }, reset() { count = 0; } }; })(); // ← 立刻执行 counter.increment(); // 1 counter.increment(); // 2 // count 变量永远是 0 吗?不,它被闭包"记住"了

大白话类比

概念类比
IIFE你开了一间旅馆房(创建了一个独立空间), 开完立刻就进去 (立即执行)
闭包你在房间里写了张纸条记着房号,退房后纸条还在(变量被记住了)
IIFE闭包开了房 → 立刻进去 → 做了些事 → 走的时候把房间号记在小本本上带走 → 下次来还能接着用

实际用途
1. 封装私有变量 — 不让外部直接访问内部状态
2. 避免命名冲突 — 每个 IIFE 都是独立的小空间
3. 循环绑定事件 — 经典的 for 循环 setTimeout 问题
简单说: IIFE 是手段,闭包是结果 ,组合起来就是「用一个一次性的函数,创建出有持久记忆的私有空间」


码字不易,点个赞再走吧~~~


RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进