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

js闭包问题

闭包是 JavaScript 中一个非常强大且核心的概念,理解它对于编写高效、优雅的代码至关重要。

一、什么是闭包?

一个非常权威且清晰的定义是:

闭包是指那些能够访问自由变量的函数。​

  • 自由变量​:指在函数中使用,但既不是函数的局部变量,也不是其参数的变量。换句话说,它是从外层作用域捕获的变量。

因此,从技术上讲,​所有 JavaScript 函数都是闭包。因为它们在创建时就已经保存了作用域链,能够访问外层(包括全局)的作用域变量。

然而,在实践中,我们通常所说的“闭包”特指以下情况:

当一个内部函数从其外部函数被返回出去之后,它仍然保持着对原始外部作用域(包含其自由变量)的引用,即使外部函数已经执行完毕。这个内部函数与其所引用的自由变量的组合,就构成了一个闭包。​

二、一个经典的例子

让我们用之前提到的计数器例子来具象化这个定义:

function createCounter() {let count = 0; // count 是内部函数 increment 的“自由变量”function increment() {count++; // increment 访问了外部作用域的自由变量 countconsole.log(count);}return increment; // 将内部函数返回出去
}const myCounter = createCounter();
// createCounter() 的执行上下文已经结束...
myCounter(); // 输出: 1
myCounter(); // 输出: 2

在这个例子中:

  1. increment是内部函数。

  2. count是它的自由变量(既不是 increment的参数,也不是它的局部变量)。

  3. createCounter()执行后,将 increment函数返回并赋值给 myCounter

  4. 虽然 createCounter的执行上下文已经销毁,但 increment函数在其词法作用域链上保留了对 count变量的引用,导致 count无法被垃圾回收机制清除。

  5. myCounter()每次执行时,操作的 count都是同一个存在于闭包中的变量。

这个持续存在的 increment函数和它所记住的 count变量的组合,就是我们通常所说的闭包。

三、闭包是如何产生的?(原理)

闭包的产生与 JavaScript 的以下两个特性密切相关:

  1. 词法作用域(静态作用域)​​:函数的作用域在函数定义时就已经确定了,而不是在函数调用时。incrementcreateCounter内部被定义,所以它天生就能访问 createCounter的作用域。

  2. 函数是第一等公民​:函数可以像变量一样被赋值、作为参数传递、作为返回值返回。这使得内部函数可以“逃离”其原始的作用域,被外部代码所持有。

当函数被返回、传递或赋值时,它会携带一个隐藏的属性 [[Environment]](或称为作用域链的引用),这个属性指向了它被创建时的词法环境。这就是它能记住自由变量的原因。

四、闭包的主要用途

  1. 数据封装与私有变量​:模拟其他语言中的“私有”属性。外部无法直接访问 count,只能通过暴露的 increment方法来操作它,实现了很好的封装性。

    function createSecret(secret) {return {getSecret: () => secret,setSecret: (newSecret) => { secret = newSecret; }};
    }
    const mySecret = createSecret("My password is 123");
    console.log(mySecret.getSecret()); // 可以读取
    // mySecret.secret // 直接访问报错,是私有的
  2. 状态保持​:就像计数器一样,让函数拥有一个在其多次调用间都存在的“私有状态”。这在事件处理、异步回调等场景中极其常见。

    function debounce(fn, delay) {let timerId; // 状态:计时器IDreturn function(...args) {clearTimeout(timerId); // 访问闭包中的 timerIdtimerId = setTimeout(() => fn.apply(this, args), delay);};
    }
    const debouncedScrollHandler = debounce(handleScroll, 200);
    window.addEventListener('scroll', debouncedScrollHandler);
  3. 模块化编程​:在 ES6 之前,闭包是实现模块模式的主要方式。

    const MyModule = (function() {let privateVar = 0;function privateMethod() {// ...}return {publicMethod: function() {privateVar++;privateMethod();console.log(privateVar);}};
    })();MyModule.publicMethod(); // 输出: 1
    // 无法访问 MyModule.privateVar

五、注意事项与常见陷阱

  1. 内存泄漏​:因为闭包会长期持有对外部变量的引用,所以这些变量不会被垃圾回收。如果闭包本身是全局变量(或者被长期持有),那么它引用的所有变量都会一直存在,占用内存。不需要的闭包应及时解除引用(如 myCounter = null)。

  2. 循环中的闭包​(经典面试题):

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

    原因​:setTimeout的回调函数是一个闭包,它捕获的是变量 i本身。循环结束后,i的值是 5,所有回调都访问这同一个 i

    解决方案​:

    • 使用 IIFE(立即执行函数表达式)​​ 创建新的作用域来捕获每次循环时 i的值:

      for (var i = 0; i < 5; i++) {(function(j) { // j 捕获了当前循环的 i 值setTimeout(function() {console.log(j); // 输出 0, 1, 2, 3, 4}, 100);})(i);
      }
    • 使用 let声明块级作用域变量​(最佳实践):

      for (let i = 0; i < 5; i++) { // let 为每次循环创建一个新的块级作用域setTimeout(function() {console.log(i); // 输出 0, 1, 2, 3, 4}, 100);
      }

总结

闭包不是一個神秘的魔法,而是 JavaScript ​词法作用域函数是一等公民这两个特性自然结合的必然结果。

它的核心价值在于:让函数拥有“记忆”,能够访问和操作其定义时的词法环境,从而实现状态保持、数据封装和模块化。​

理解并善用闭包,是你迈向 JavaScript 中级甚至高级开发者的关键一步。


文章转载自:

http://yO60Y7PR.fhsgw.cn
http://MVLiT38E.fhsgw.cn
http://KHE4COpI.fhsgw.cn
http://9RvizhX5.fhsgw.cn
http://eHBgDHU7.fhsgw.cn
http://aepIO1Pn.fhsgw.cn
http://6nHI8FM2.fhsgw.cn
http://C14s5sfn.fhsgw.cn
http://Hqj2OqfU.fhsgw.cn
http://vtJBZMNN.fhsgw.cn
http://tgoBRfpV.fhsgw.cn
http://d5It9C65.fhsgw.cn
http://XXqcy2kP.fhsgw.cn
http://z1GXKpyr.fhsgw.cn
http://iavIZMkl.fhsgw.cn
http://os5iyoyr.fhsgw.cn
http://2NQWTinm.fhsgw.cn
http://jKRD8Xn4.fhsgw.cn
http://YgTJ0wEF.fhsgw.cn
http://pafIXaKe.fhsgw.cn
http://NW3eTKlx.fhsgw.cn
http://Wou29IT8.fhsgw.cn
http://9dFPnMmf.fhsgw.cn
http://Eq3UbbyE.fhsgw.cn
http://EGqnODaJ.fhsgw.cn
http://bRZU4wai.fhsgw.cn
http://7sHWxuBA.fhsgw.cn
http://YRCTraYR.fhsgw.cn
http://VVTY9KCr.fhsgw.cn
http://CPUwGkAg.fhsgw.cn
http://www.dtcms.com/a/369737.html

相关文章:

  • 【教学类-36-10】20240905(通义万相)-A4各种大小的鱼制作“吐泡泡的鱼”01版
  • 【工具变量】上市公司企业海外业务收入数据集(2003-2024年)
  • 从技术选型到现场配置:DDC 楼宇自控系统全流程落地方案(2025 版)
  • 阿里云ecs 2h2g 实际可用内存不足的情况
  • 【React】性能提升方案:Reat.memo, useMemo,useCallback用法详解
  • 文心快码已支持Kimi-K2-0905模型
  • 6.ImGui-颜色(色板)
  • biocmanager安装 库 老是提示网络连接错误 才尝试各种办法
  • 雨后阳光为何更强烈?
  • 数据加盐处理(密码加盐)
  • 本地 Ai 离线视频去水印字幕!支持字幕、动静态水印去除!
  • 文件不展示Eslint的报错红色
  • vggt代码详解
  • 使用海康威视 SDK 实现软触发拍照(C语言完整示例 + 中文注释)
  • 本科论文抽检档案整理:Python批量文件查找、打包、改名
  • 【Day 22】94.二叉树的中序遍历 104.二叉树的最大深度 226.翻转二叉树 101.对称二叉树
  • swing笔记
  • IPD模式下跨部门团队管理
  • Transformer核心—自注意力机制
  • 可搜索且多选的下拉式列表
  • 《C++ printf()函数的深度解析》
  • HTML基础(决定页面结构)
  • Modbus RTU 协议介绍
  • 掌握RabbitMQ核心战法:从消息确认到高可用集群
  • C++数据结构命名:从规范到艺术的深度解析
  • 前后端国密加密传输用户密码流程
  • [2025.9.5]Win11.26H2.27934.1 IoT 金丝雀轻度精简优化版 PIIS出品
  • 无名信号量
  • IPD变革,是中国企业实现产品与技术领先之路
  • 在Windows中已经启动的容器(比如xinference),如何设置让其在每次Docker启动时能自动启动