当前位置: 首页 > news >正文

JavaScript-ES5 循环中的闭包 “共享变量” 问题

前置知识

ES5中只有function是作用域

ES5之前因为if和for都没有块级作用域的概念,所以在很多时候,我们都必须借助于function的作用域来解决应用外面变量的问题


ES6中加入了let,let它是有if和for的块级作用域

ES5中的var是没有块级作用域(if/for)
ES6中的let是有块级作用域的(if/for)

一.for循环

1.示例一

function fn() {for (var i = 0; i < 3; i++) { // 终止条件:i < 3console.log(i); }
}
fn();

⑴.步骤拆解

  1. 初始化

    • var i = 0,此时 i 的值为 0

  2. 第一次循环迭代

    • 检查条件i < 3 → 0 < 3 → true

    • 执行循环体console.log(i) → 输出 0

    • 执行增量i++ → i 变为 1

  3. 第二次循环迭代

    • 检查条件i < 3 → 1 < 3 → true

    • 执行循环体console.log(i) → 输出 1

    • 执行增量i++ → i 变为 2

  4. 第三次循环迭代

    • 检查条件i < 3 → 2 < 3 → true

    • 执行循环体console.log(i) → 输出 2

    • 执行增量i++ → i 变为 3

  5. 第四次条件检查

    • 检查条件i < 3 → 3 < 3 → false

    • 循环终止,不再执行循环体。

⑵.关键点

  • 循环终止条件i < 3,当 i 增长到 3 时条件不满足,循环结束。

  • 循环体的执行时机console.log(i) 在每次循环迭代的 条件检查之后、增量操作之前 执行。

  • i 的实际最大值

    • 循环结束后 i 的值为 3(可以通过在循环外打印 i 验证)。

    • 但循环体最后一次执行时,i 的值为 2(增量操作 i++ 在循环体执行后才生效)。

2.验证 i 的最终值

你可以在循环外打印 i,观察它的最终值:

function fn() {for (var i = 0; i < 3; i++) {console.log("循环内:", i); // 输出 0、1、2}console.log("循环外:", i); // 输出 3
}
fn();

输出结果:

二、循环中的闭包“共享变量”问题


在 ES5 环境中,循环中的闭包“共享变量”问题是一个常见的陷阱。以下是详细解释和解决方案:


1.问题现象

for 循环中使用 var 声明变量时,闭包(如回调函数)会捕获循环变量的引用,而非其当前值。当循环结束后,所有闭包共享同一个变量,导致最终输出的值是循环结束后的最终值(而非预期的迭代值)。

示例代码(ES5):

for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i); // 所有输出都是 5}, 100);
}

输出结果:

5
5
5
5
5

解释:



关键原因总结

  • 变量作用域问题:

var 声明的变量具有函数作用域,而非块级作用域(let/const 是 ES6 的块级作用域)。因此,所有 setTimeout 的回调函数共享同一个 i 变量。

  • 异步执行的延迟:

setTimeout 是异步操作,其回调函数的执行被推迟到事件循环的下一轮。此时,循环已经完成,i 的最终值是 5,所有回调函数访问的都是这个最终值。


详细过程分析

步骤 1:循环的同步执行
  • 初始状态i 的初始值是 0,作用域是整个函数(或全局作用域)。
  • 循环执行
    • 第一次循环(i = 0):
      • 设置 setTimeout,但回调函数不会立即执行,而是被推入浏览器的任务队列中。
    • 第二次循环(i = 1):
      • 再次设置 setTimeout,回调函数被推入任务队列。
    • 以此类推,直到 i = 4 时,循环结束。
    • 循环结束后i 的值变为 5(因为循环条件 i < 5 不成立时才退出循环)。
步骤 2:异步回调的执行
  • 同步代码执行完毕:循环结束后,所有同步代码(如 for 循环本身)已经执行完成。
  • 事件循环触发:浏览器的事件循环开始处理任务队列中的 setTimeout 回调函数。
  • 回调函数执行时每个回调函数访问的 i 是同一个变量,此时 i 的值已经是 5(循环结束后的最终值)。

关键点详解

1. 变量作用域与闭包
  • var 的函数作用域
    var i 声明的变量在整个函数或全局作用域中唯一,所有 setTimeout 的回调函数共享这个变量。
  • 闭包捕获变量引用
    每个 setTimeout 的回调函数形成了一个闭包,它捕获的是外部变量 i 的引用,而非 i 的当前值。因此,当回调函数执行时,它们访问的始终是同一个 i 的最新值(即 5)。
2. 异步与事件循环
  • 异步任务的延迟
    setTimeout 的回调函数会被放入浏览器的宏任务队列中,等待当前同步代码执行完毕后才会执行。
  • 事件循环的执行顺序
    浏览器的事件循环机制会先执行所有同步代码(如 for 循环),然后处理任务队列中的异步任务。此时,i 的值已经是 5,所有回调函数输出的都是 5

可视化时间线

时间阶段代码执行内容
同步阶段循环快速执行完毕,i 的值变为 5。所有 setTimeout 回调被推入任务队列。
事件循环触发任务队列中的回调开始执行,此时 i = 5,所有回调输出 5

2.解决方案

如果希望每个 setTimeout 输出对应的 i 值(如 0,1,2,3,4),可以通过以下方法隔离变量:

方法 1:使用 let(ES6)

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

解释


关键问题:为什么输出是 0,1,2,3,4 而不是 5,5,5,5,5?

核心原因

  • let 的块级作用域:

let 声明的变量在每次循环迭代中都会创建一个新的块级作用域,因此每次迭代的 i 是独立的变量。

  • 闭包捕获独立变量:

每个 setTimeout 的回调函数捕获的是当前迭代的 i 变量,而非所有迭代共享的同一个变量。


详细过程分析
步骤 1:循环的同步执行
  • 初始状态
    for 循环开始执行,let i = 0 在第一次迭代时创建了一个块级作用域,其中 i 的初始值为 0

  • 循环迭代
    每次循环迭代时,let 会为 i 创建一个新的块级作用域。例如:

    • 第一次迭代
      • 块级作用域 Scope1 中 i = 0
      • 设置 setTimeout,回调函数被推入任务队列,但不会立即执行
    • 第二次迭代
      • 块级作用域 Scope2 中 i = 1
      • 再次设置 setTimeout,回调函数被推入任务队列。
    • 以此类推,直到 i = 4 时,循环结束。
  • 循环结束后
    i 的最终值变为 5(因为循环条件 i < 5 不成立时退出循环),但此时所有迭代的 i 变量(Scope1Scope5)仍然独立存在。

步骤 2:异步回调的执行
  • 同步代码执行完毕
    循环结束后,所有同步代码(如 for 循环本身)已经执行完成,浏览器开始处理任务队列。

  • 事件循环触发
    浏览器的事件循环开始处理宏任务队列中的 setTimeout 回调函数。此时,每个回调函数访问的 i各自迭代时的块级作用域中的变量,而非最终的 i = 5

  • 回调函数执行时的 i
    每个回调函数捕获的是其所在迭代的 i 变量的引用:

    • 第一个回调函数捕获的是 Scope1 中的 i = 0
    • 第二个回调函数捕获的是 Scope2 中的 i = 1
    • 以此类推,最终输出 0,1,2,3,4

关键机制详解
1. let 的块级作用域
  • 块级作用域的创建
    for 循环的每次迭代都会生成一个新的块级作用域。例如:

for (let i = 0; i < 5; i++) {// 每次迭代的代码块是一个独立的块级作用域
}
    • 每次迭代的代码块({})内部,let i 声明的变量 i 是独立的,与其他迭代的 i 无关。
  • 变量隔离
    每次迭代的 i 存在于各自的块级作用域中,因此不会相互干扰。

2. 闭包与词法作用域
  • 闭包的形成
    每个 setTimeout 的回调函数形成一个闭包,捕获其所在迭代的块级作用域中的 i 变量。例如:

setTimeout(function() { console.log(i); }, 100);
  • 回调函数的词法作用域链指向其定义时的块级作用域(如 Scope1Scope2 等)。
  • 变量引用的独立性
    每个闭包访问的是独立的 i 变量,而非循环结束后共享的变量。

3. 异步任务的延迟执行
  • 任务队列的处理顺序
    setTimeout 的回调函数属于宏任务,会被推入宏任务队列。事件循环会在同步代码执行完毕后,依次执行队列中的任务。

  • 闭包执行时的变量值
    当回调函数执行时,它访问的是其闭包中捕获的 i 的当前值(即定义时的值),而非循环结束后的最终值。

对比 var 的情况

如果代码使用 var 替代 let,则会出现问题:

for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i); // 输出 5,5,5,5,5}, 100);
}
  • var 的函数作用域
    var 声明的 i 具有函数作用域(或全局作用域),所有迭代共享同一个 i 变量。
  • 闭包捕获共享变量
    所有回调函数捕获的是同一个 i 的引用,最终值为 5,因此输出全为 5
总结
代码作用域闭包捕获的变量输出结果
for (let i = 0; ...块级作用域(每次迭代)当前迭代的独立 i 变量0,1,2,3,4
for (var i = 0; ...函数作用域共享的最终 i = 55,5,5,5,5

方法 2:立即执行函数(IIFE)(ES5 兼容)

for (var i = 0; i < 5; i++) {(function(j) { // 通过参数 j 传递当前的 i 值setTimeout(function() {console.log(j); // 每次迭代的 j 是独立的}, 100);})(i); // 立即执行函数并传入当前 i
}
  • 原理:通过 IIFE 为每次迭代创建独立的作用域,j 是当前 i 的值,闭包捕获的是 j 的独立副本。

解释



逐行解释
1. 外层循环(for 循环)
for (var i = 0; i < 5; i++) {// ...
}
  • 这是一个普通的 for 循环,使用 var 声明变量 i
  • 问题var 的作用域是函数级的,所有循环迭代共享同一个 i 变量。
2. 立即执行函数(IIFE)
(function(j) { // 定义一个函数,参数是 j// 函数体
})(i); // 立即执行这个函数,并传入当前的 i 值
  • 这是一个立即执行函数表达式(IIFE),每次循环迭代时都会创建一个独立的作用域
  • 关键点:通过参数 j 将当前 i 的值传递给函数内部,这样每个 IIFE 的作用域中都有自己的 j 变量。
3. setTimeout 回调函数
setTimeout(function() {console.log(j); 
}, 100);
  • 这是延迟执行的代码块(异步操作)。
  • 闭包的作用:这个回调函数会捕获其所在 IIFE 的 j 变量,而不是外层循环的 i 变量。

执行过程(分步详解)

步骤 1:循环开始
  • 初始状态i = 0(外层循环的 var i)。
  • 第一次迭代
    1. 执行 i = 0
    2. 调用 IIFE:(function(j) { ... })(0)
      • 参数 j 被赋值为当前 i 的值(即 0)。
      • 这个 IIFE 的作用域中,j = 0
    3. 设置 setTimeout,回调函数被推入任务队列,但不会立即执行
步骤 2:后续迭代
  • 第二次迭代
    1. i 增加到 1
    2. 调用 IIFE:(function(j) { ... })(1)
      • 参数 j 被赋值为 1
      • 这个 IIFE 的作用域中,j = 1
    3. 再次设置 setTimeout,回调函数被推入任务队列。
  • 以此类推,直到 i = 4 时,循环结束。
步骤 3:循环结束后
  • 外层循环的 i 最终变为 5(因为循环条件 i < 5 不成立时退出)。
  • 所有 setTimeout 的回调函数仍在任务队列中等待执行。
步骤 4:异步回调执行
  • 事件循环触发:当 100ms 到达后,浏览器开始执行这些回调函数。
  • 每个回调函数的执行环境
    • 第一个回调函数捕获的是第一次迭代的 j = 0
    • 第二个回调函数捕获的是第二次迭代的 j = 1
    • 以此类推,最终输出 0, 1, 2, 3, 4

为什么这样能解决问题?

核心原因:作用域隔离
  1. IIFE 创建独立作用域
    每次循环迭代时,通过 IIFE 创建了一个新的作用域,每个作用域都有自己的 j 变量。
  2. 闭包捕获独立变量
    每个 setTimeout 的回调函数捕获的是当前 IIFE 的 j,而非外层共享的 i
  3. 避免“最终值”问题
    即使外层的 i 最终变为 5,每个回调函数的 j 仍保留了当时传递的值(如 01 等)。

http://www.xdnf.cn/news/67285.html

相关文章:

  • 部署本地Dify
  • 智能安全用电系统预防电气线路老化、线路或设备绝缘故障
  • Windows部署FunASR实时语音听写便捷部署教程
  • Python Cookbook-6.6 在代理中托管特殊方法
  • AI重塑网络安全:机遇与威胁并存的“双刃剑”时代
  • CI/CD
  • Servlet上传文件
  • 2025年渗透测试面试题总结-拷打题库10(题目+回答)
  • 软考 中级软件设计师 考点知识点笔记总结 day14 关系代数 数据库完整性约束
  • 计算机是如何工作的(上)
  • 23. git reset
  • 【形式化验证基础】活跃属性Liveness Property和安全性质(Safety Property)介绍
  • 计算机组成与体系结构:内存接口(Memory Interface)
  • 工具:下载vscode .vsix扩展文件及安装的方法
  • 可穿戴设备待机功耗需降至μA级但需保持实时响应(2万字长文深度解析)
  • 小天互连与DeepSeek构建企业智能化新生态
  • iframe下系统访问跨域问题解决办法
  • VTK知识学习(53)- 交互与Widget(四)
  • Unity3D ILRuntime与Scripting Backend整合指南
  • 剪映学习02
  • Kotlin协程学习笔记
  • OpenCV---图像预处理(四)
  • HCIE Datacom备考技巧
  • typedef MVS_API CLISTDEF0IDX(ViewScore, IIndex) ViewScoreArr;
  • 《解锁增强型上下文学习,打造你的专属智能助手》
  • 每天学一个 Linux 命令(29):tail
  • gnome中删除application中失效的图标
  • 齐次坐标系下的变换矩阵
  • 【图论 DFS BFS】P10725 [GESP202406 八级] 最远点对|普及+
  • LangChain实现PDF中图表文本多模态数据向量化及RAG应用实战指南