news 2026/5/1 9:01:14

JavaScript学习笔记:17.闭包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript学习笔记:17.闭包

JavaScript学习笔记:17.闭包

上一篇用模块搞定了代码的“部门分工”,这一篇咱们来攻克JS中“既神秘又核心”的概念——闭包(Closures)。你可能听过“闭包能保留变量”“闭包会导致内存泄漏”,但始终没搞懂它到底是什么;也可能在写防抖节流、私有变量时无意中用过它,却不知道背后的原理。

其实闭包一点都不玄乎,它就像函数的“带钥匙管家”:外部函数(相当于“主人”)执行完后,作用域本应“关门大吉”,但内部函数(“管家”)偷偷揣着“钥匙”(引用外部函数的变量),即使主人走了,管家还能随时打开门,访问里面的“财物”(变量)。今天咱们就用“房间与钥匙”的生活化比喻,把闭包的形成原理、实战用法、优缺点和避坑指南彻底讲透,让你从“模糊感知”到“熟练运用”。

一、先破案:为什么需要闭包?普通函数的痛点

普通函数执行完后,其作用域会被垃圾回收机制销毁,里面的变量也会随之消失,就像主人出门后,房间被清空,再回来啥都没了。但开发中经常需要“保留函数执行后的状态”,比如:

  • 计数器:多次调用函数,累加同一个变量(不是每次都从0开始);
  • 私有变量:不想让外部直接访问的变量,只能通过特定方法操作;
  • 防抖节流:需要记住上一次执行的时间,判断是否触发下一次。

这些场景普通函数搞不定,而闭包能完美解决——它能让函数执行后,作用域不被销毁,变量被“偷偷保留”下来。

看个直观例子:普通函数vs闭包函数

// 普通函数:执行后变量消失,无法累加functionnormalCounter(){letcount=0;count++;returncount;}console.log(normalCounter());// 1console.log(normalCounter());// 1(count每次都重新初始化,无法累加)// 闭包函数:执行后变量被保留,实现累加functionclosureCounter(){letcount=0;// 外部函数变量// 内部函数:引用外部函数的countreturnfunction(){count++;returncount;};}constcounter=closureCounter();// 外部函数执行,返回内部函数(管家带钥匙出门)console.log(counter());// 1(管家开门,count=1)console.log(counter());// 2(管家再开门,count=2)console.log(counter());// 3(变量被持续保留)

核心差异:普通函数的变量随作用域销毁,闭包的变量被内部函数引用,作用域不销毁,变量持续可用。

二、闭包的核心原理:什么是闭包?怎么形成的?

1. 闭包的定义(MDN官方)

闭包是指一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。简单说,闭包让内部函数可以访问外部函数的作用域,即使外部函数已经执行完毕。

用“房间比喻”翻译:

  • 外部函数 = 主人的房间;
  • 外部函数变量 = 房间里的财物;
  • 内部函数 = 带钥匙的管家;
  • 闭包 = 管家+钥匙+房间的组合(即使主人走了,管家还能开门取财物)。

2. 闭包的形成条件(缺一不可)

闭包不是“写个嵌套函数就是闭包”,必须满足三个条件:

  1. 函数嵌套:存在内部函数和外部函数(嵌套关系);
  2. 变量引用:内部函数引用了外部函数的变量/参数(管家拿了钥匙);
  3. 外部暴露:外部函数执行后,内部函数被外部引用(管家出门了,没被留在房间里)。
// 满足三个条件:闭包形成functionouter(){constouterVar="我是外部变量";// 外部函数变量functioninner(){console.log(outerVar);// 内部函数引用外部变量(条件2)}returninner;// 外部暴露内部函数(条件3)}constinnerFunc=outer();// 外部函数执行,内部函数被外部引用innerFunc();// 打印"我是外部变量"(闭包生效)// 不满足条件3:无闭包(内部函数没被外部引用)functionouter2(){constouterVar="我是外部变量";functioninner2(){console.log(outerVar);}inner2();// 内部函数在外部函数内执行,没被外部引用}outer2();// 执行后outer2作用域销毁,无闭包

3. 闭包的执行过程:为什么变量能保留?

咱们拆解closureCounter的执行过程,看懂闭包的“魔法”:

  1. 调用closureCounter()(外部函数执行):
    • 创建外部函数作用域,声明count=0
    • 外部函数返回内部函数(此时内部函数引用了count);
    • 外部函数执行完毕,但因为内部函数还引用着它的变量,作用域不会被垃圾回收(房间没被清空)。
  2. 调用counter()(内部函数执行):
    • 内部函数执行时,先在自己的作用域找count,找不到;
    • 顺着作用域链,找到外部函数的作用域(因为闭包保留了引用);
    • 访问并修改count,实现累加。
  3. 多次调用counter():重复步骤2,count持续被保留和修改。

简单说:闭包的本质是“作用域链的延长”——内部函数的作用域链,永远保留着对外部函数作用域的引用,即使外部函数执行完。

三、闭包的实战场景:这些功能离不开闭包

闭包不是“炫技工具”,而是很多核心功能的底层实现,以下三个场景几乎是前端开发的“必备技能”。

1. 场景1:实现私有变量(隐藏内部状态)

JS没有原生的私有变量(ES6的#私有字段是后来加的),闭包是传统实现“私有变量”的唯一方式——让变量只能通过特定方法访问,不能被外部直接修改,保证数据安全。

// 用闭包实现“安全的银行账户”(私有变量:余额)functioncreateBankAccount(initialBalance){// 私有变量:外部无法直接访问letbalance=initialBalance;// 暴露对外接口(闭包函数)return{deposit(amount){// 存款:只能通过这个方法修改余额if(amount>0){balance+=amount;return`存款成功,余额:${balance}`;}return"存款金额无效";},withdraw(amount){// 取款:只能通过这个方法修改余额if(amount>0&&amount<=balance){balance-=amount;return`取款成功,余额:${balance}`;}return"取款金额无效";},getBalance(){// 查询余额:只能通过这个方法访问余额return`当前余额:${balance}`;}};}// 使用账户constaccount=createBankAccount(1000);console.log(account.getBalance());// "当前余额:1000"console.log(account.deposit(500));// "存款成功,余额:1500"console.log(account.withdraw(300));// "取款成功,余额:1200"// 外部无法直接访问私有变量console.log(account.balance);// undefined(无法直接访问)account.balance=100000;// 无效,修改的是对象的新属性,不是闭包中的balanceconsole.log(account.getBalance());// "当前余额:1200"(余额未变)

2. 场景2:防抖与节流(前端性能优化神器)

防抖(debounce)和节流(throttle)是解决“高频事件触发”(如滚动、输入、点击)的核心方案,它们的底层都依赖闭包保留“上次执行时间”“计时器ID”等状态。

(1)防抖:触发后延迟n秒执行,期间再次触发则重新计时
// 防抖函数(闭包实现)functiondebounce(fn,delay){lettimerId;// 闭包保留计时器IDreturnfunction(...args){// 再次触发时,清除之前的计时器clearTimeout(timerId);// 重新设置计时器,延迟执行timerId=setTimeout(()=>{fn.apply(this,args);// 执行原函数},delay);};}// 用法:输入框搜索,避免输入时频繁请求接口constsearchInput=document.querySelector('input');functionsearch(keyword){console.log(`搜索:${keyword}`);// 实际开发中这里是接口请求}// 给搜索函数加防抖,延迟500msconstdebouncedSearch=debounce(search,500);searchInput.addEventListener('input',(e)=>{debouncedSearch(e.target.value);});

闭包在这里的作用:保留timerId,让每次触发时都能访问到上一次的计时器,实现“清除旧计时器、设置新计时器”。

(2)节流:n秒内只执行一次,避免频繁触发
// 节流函数(闭包实现)functionthrottle(fn,interval){letlastTime=0;// 闭包保留上次执行时间returnfunction(...args){constcurrentTime=Date.now();// 距离上次执行时间超过interval,才执行if(currentTime-lastTime>=interval){fn.apply(this,args);lastTime=currentTime;// 更新上次执行时间}};}// 用法:滚动事件,避免滚动时频繁触发functionhandleScroll(){console.log('滚动触发');// 实际开发中这里是滚动加载、位置计算等}// 给滚动函数加节流,1秒内只执行一次constthrottledScroll=throttle(handleScroll,1000);window.addEventListener('scroll',throttledScroll);

3. 场景3:函数工厂(批量生成带状态的函数)

用闭包批量生成具有相同逻辑但不同状态的函数,比如批量生成“带固定前缀的日志函数”“带特定权限的接口请求函数”。

// 函数工厂:生成带固定前缀的日志函数functioncreateLogger(prefix){// 闭包保留前缀returnfunction(message){consttime=newDate().toLocaleTimeString();console.log(`[${prefix}][${time}]${message}`);};}// 批量生成不同类型的日志函数constinfoLogger=createLogger('INFO');consterrorLogger=createLogger('ERROR');constwarnLogger=createLogger('WARN');// 使用infoLogger('用户登录成功');// [INFO][14:30:00] 用户登录成功errorLogger('接口请求失败');// [ERROR][14:30:05] 接口请求失败warnLogger('参数格式错误');// [WARN][14:30:10] 参数格式错误

每个日志函数都保留着自己的prefix,逻辑相同但状态独立,这就是闭包的“状态隔离”能力。

四、闭包的优缺点:一把“双刃剑”

闭包很强大,但不是万能的,它是一把“双刃剑”,有优点也有需要警惕的缺点。

1. 优点

  • 保留状态:函数执行后保留变量,实现累加、缓存等功能;
  • 数据私有:隐藏内部变量,只暴露指定接口,保证数据安全;
  • 逻辑复用:批量生成带状态的函数,减少重复代码。

2. 缺点(重点避坑)

  • 内存泄漏风险:闭包引用的外部函数变量不会被垃圾回收,若长期不释放,会占用额外内存,严重时导致内存泄漏;
  • 调试困难:闭包延长了作用域链,变量的生命周期变得复杂,调试时难以追踪变量的修改路径。

五、避坑指南:正确使用闭包,避免踩雷

1. 避坑1:避免不必要的闭包,及时释放引用

不需要保留状态时,不要用闭包;闭包不用时,手动解除引用,让垃圾回收机制回收变量:

// 反面例子:长期持有闭包,不释放constcounter=closureCounter();// ... 不再使用counter,但没有解除引用// 闭包中的count会一直存在,占用内存// 正面例子:不用时解除引用letcounter=closureCounter();counter();// 使用counter=null;// 解除引用,闭包中的变量会被垃圾回收

2. 避坑2:循环中的闭包陷阱(经典面试题)

这是闭包最经典的坑:用var声明循环变量,闭包会引用同一个变量,导致所有内部函数执行时拿到的都是最后一个值。

// 反面例子:循环中的闭包陷阱constarr=[];for(vari=0;i<3;i++){arr.push(function(){console.log(i);// 内部函数引用循环变量i(var声明,全局作用域)});}// 执行数组中的函数arr[0]();// 3arr[1]();// 3arr[2]();// 3(所有函数拿到的都是i的最终值3)

原因var声明的i是全局变量,循环中所有内部函数引用的都是同一个i,循环结束后i=3,所以执行时都打印3。

解决方案

  • let声明循环变量(let有块级作用域,每次循环都会创建新的i);
  • 用立即执行函数(IIFE)创建独立作用域。
// 解决方案1:用let声明(推荐)constarr=[];for(leti=0;i<3;i++){arr.push(function(){console.log(i);// 每个函数引用的是当前循环的i(块级作用域)});}arr[0]();// 0arr[1]();// 1arr[2]();// 2// 解决方案2:用IIFE(兼容旧环境)constarr=[];for(vari=0;i<3;i++){(function(j){arr.push(function(){console.log(j);// 每个函数引用的是IIFE的参数j(独立作用域)});})(i);// 立即执行,传入当前i的值}arr[0]();// 0arr[1]();// 1arr[2]();// 2

3. 避坑3:闭包不要引用过大的变量

闭包会保留外部函数的整个作用域,而不是只保留引用的变量。如果外部函数有很多大变量(比如大数组、大对象),即使内部函数只引用了一个小变量,整个作用域的变量都会被保留,导致内存浪费。

// 反面例子:闭包引用外部函数的大变量functionouter(){constbigData=newArray(1000000).fill(0);// 大数组,占用大量内存constsmallVar="我只需要这个";returnfunction(){console.log(smallVar);// 只引用了smallVar,但bigData也会被保留};}// 正面例子:只保留需要的变量,避免引用大变量functionouter(){constsmallVar="我只需要这个";// 大变量放在不需要的地方,或手动释放constbigData=newArray(1000000).fill(0);bigData=null;// 手动释放大变量returnfunction(){console.log(smallVar);};}

六、总结:闭包的核心价值与本质

闭包的本质是“作用域链的延长与保留”,核心价值是“让函数突破作用域的限制,保留执行状态”。它不是JS的“特殊功能”,而是作用域和作用域链的自然产物——只要满足“嵌套函数、引用外部变量、外部暴露内部函数”三个条件,闭包就会自动形成。

记住三个核心点:

  1. 闭包是“带钥匙的管家”:保留外部函数的变量,即使外部函数执行完;
  2. 闭包的核心用法:私有变量、防抖节流、函数工厂;
  3. 闭包的使用原则:按需使用,及时释放,避免引用过大变量和循环陷阱。

闭包是JS的“进阶门槛”,掌握它不仅能写出更优雅、更安全的代码,还能理解很多前端框架和工具的底层实现(比如React的hooks、Vue的响应式)。下一篇笔记,咱们会聊JS的“异步进阶”——Async/Await,解锁更简洁的异步编程方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 4:52:09

视频直播点播平台EasyDSS构建高并发、低延迟的远程教学直播新模式

随着在线教育的常态化推进&#xff0c;远程教学已成为学校教育与职业培训的重要补充形式。无论是K12阶段的同步课堂、高校的公开课直播&#xff0c;还是企业的内部培训&#xff0c;都对视频直播的稳定性、互动性、多终端适配性提出了严苛要求。EasyDSS作为一款成熟的视频直播点…

作者头像 李华
网站建设 2026/4/29 7:59:06

大数据量 Excel 导入的性能与内存优化实战

大数据量 Excel 导入的性能与内存优化实战 一 核心原则 使用流式/事件驱动读取&#xff08;如 EasyExcel、POI SAX&#xff09;&#xff0c;避免 XSSFWorkbook 一次性将整表加载进内存&#xff0c;内存占用可做到与文件大小基本无关。采用分批处理 批量写入&#xff0c;每批积…

作者头像 李华
网站建设 2026/5/1 4:54:43

AI大模型通用技术架构图,非常详细收藏我这一篇就够了

AI大模型通用技术架构图​最后 我在一线科技企业深耕十二载&#xff0c;见证过太多因技术卡位而跃迁的案例。那些率先拥抱 AI 的同事&#xff0c;早已在效率与薪资上形成代际优势&#xff0c;我意识到有很多经验和知识值得分享给大家&#xff0c;也可以通过我们的能力和经验解答…

作者头像 李华
网站建设 2026/5/1 6:11:48

小白入门大模型产品经理:完整学习路线与免费资源大礼包_LLM大模型产品经理学习路线

文章提供了大模型产品经理的完整学习路线&#xff0c;包括基础知识、大模型技术、产品管理、实战经验和持续提升五个阶段。同时介绍了七阶段AI大模型学习计划&#xff0c;涵盖系统设计、提示词工程、平台应用开发等内容。文章还提供了一系列免费学习资源&#xff0c;包括学习路…

作者头像 李华