JavaScript 闭包:函数背后的“背包”
JavaScript 闭包函数背后的“背包” 什么是闭包官方定义闭包是指有权访问另一个函数作用域中变量的函数。通俗解释想象一个函数是一个人他出生时背了一个背包Scope/作用域。这个背包里装着他出生时周围环境中的所有变量。即使这个人走到了世界的尽头全局环境或者他的父母外部函数已经去世执行完毕他依然背着那个背包可以随时拿出里面的东西使用。一句话总结闭包 函数 函数能访问到的自由变量背包里的东西。 目录 核心原理为什么会有闭包️ 闭包的三大核心作用 代码实战经典案例解析⚠️ 双刃剑内存泄漏与性能 总结1. 核心原理为什么会有闭包要理解闭包首先要理解 JS 的词法作用域Lexical Scoping和垃圾回收机制。正常情况函数执行完变量销毁functionouter(){leta10;console.log(a);}outer();// 10// 函数执行完毕a 被垃圾回收器回收内存释放。特殊情况内部函数引用了外部变量functionouter(){leta10;functioninner(){console.log(a);// inner 引用了 outer 的变量 a}returninner;// 将 inner 返回出去}constmyFuncouter();myFunc();// 10发生了什么outer执行完毕按理说a应该被销毁。但是inner函数被返回并赋值给了myFunc。inner的定义中包含了对a的引用。JS 引擎发现“嘿inner还在外面被人拿着呢它还要用a所以我不能销毁a”于是a所在的内存空间被保留下来形成了闭包。2. ️ 闭包的三大核心作用闭包不仅仅是理论它在实际开发中有三个非常强大的用途✅ 作用一数据封装与私有变量模拟私有属性在 ES6 Class 出现之前JS 没有真正的“私有变量”。闭包是实现模块化和数据隐藏的核心手段。场景创建一个计数器外部只能调用increment无法直接修改count。functioncreateCounter(){letcount0;// 私有变量外部无法直接访问return{increment:function(){count;returncount;},decrement:function(){count--;returncount;},getCount:function(){returncount;},};}constcountercreateCounter();console.log(counter.increment());// 1console.log(counter.increment());// 2console.log(counter.count);// undefined ✅ 无法直接访问价值保护数据不被意外篡改提供清晰的 API 接口。✅ 作用二函数柯里化与参数复用闭包可以“记住”之前传入的参数从而实现函数的部分应用Partial Application。场景固定税率计算。functionmakeTaxCalculator(taxRate){// taxRate 被闭包“记住”了returnfunction(price){returnprice*taxRate;};}constcalcVATmakeTaxCalculator(0.13);// 增值税率 13%constcalcIncomeTaxmakeTaxCalculator(0.2);// 所得税率 20%console.log(calcVAT(100));// 13console.log(calcIncomeTax(100));// 20价值减少重复传参提高代码复用性。✅ 作用三异步回调中的状态保持在异步编程如定时器、AJAX、事件监听中闭包确保回调函数执行时依然能访问到定义时的上下文变量。场景延迟打印日志。functionlogAfterDelay(msg,delay){setTimeout(function(){// 这里的 msg 和 delay 即使在 setTimeout 触发时几秒后依然可用console.log(${msg}after${delay}ms);},delay);}logAfterDelay(Hello,1000);3. 代码实战经典面试题解析❌ 经典陷阱循环中的闭包这是面试中最常考的闭包问题。for(vari1;i5;i){setTimeout(functiontimer(){console.log(i);},i*1000);}预期输出1, 2, 3, 4, 5实际输出6, 6, 6, 6, 6 原因var没有块级作用域所有定时器共享同一个全局变量i。当定时器执行时循环已经结束i变成了 6。✅ 解决方案 1使用 IIFE立即执行函数创建闭包for(vari1;i5;i){(function(j){setTimeout(functiontimer(){console.log(j);// j 是每次循环独立的副本},j*1000);})(i);}原理IIFE 为每次循环创建了一个新的作用域将当前的i值作为参数j传入并保存。✅ 解决方案 2使用let推荐for(leti1;i5;i){setTimeout(functiontimer(){console.log(i);},i*1000);}原理let具有块级作用域JS 引擎会为每次循环迭代创建一个新的绑定。4. ⚠️ 双刃剑内存泄漏与性能闭包虽然强大但滥用会导致内存泄漏。 为什么会导致内存泄漏正常情况下函数执行完后局部变量会被回收。但如果形成了闭包且外部一直持有对该闭包函数的引用那么闭包所引用的所有外部变量都不会被回收。危险示例functionhugeDataHandler(){consthugeArraynewArray(1000000).fill(data);// 占用大量内存returnfunction(){console.log(hugeArray.length);// 只要这个函数存在hugeArray 就不会被回收};}consthandlerhugeDataHandler();// 如果你不再需要 handler必须手动解除引用handlernull;// ✅ 允许 GC 回收 hugeArray️ 最佳实践及时解除引用如果不再需要闭包函数将其赋值为null。避免在闭包中引用巨大的无用对象。谨慎使用全局闭包尽量缩小闭包的作用范围。 总结特性说明本质函数 引用的自由变量核心能力延长变量生命周期实现数据私有化主要用途1. 封装私有变量2. 柯里化/参数复用3. 异步回调状态保持副作用可能导致内存泄漏增加内存占用解决循环陷阱使用let或 IIFE 博主寄语闭包不是魔法它是 JS 作用域机制的自然产物。不要为了用闭包而用闭包。当你需要隐藏数据、记住状态或复用逻辑时闭包就是你的最佳伙伴。记住口诀函数嵌套生闭包外部变量怀里抱。私有数据能封装异步回调少不了。用完记得清引用内存泄漏要防好。希望这篇文档能帮你彻底搞懂闭包的作用如果有疑问欢迎在评论区留言。喜欢这篇文章吗记得点赞、收藏、转发哦❤️