前端-闭包
闭包(Closure)是JavaScript中一个非常重要且强大的概念理解闭包对于掌握JavaScript高级特性至关重要。下面我将从多个角度详细解释闭包的概念、原理和应用。一、什么是闭包闭包是指那些能够访问独立 (自由) 变量的函数或者说函数定义时的词法环境lexical environment而非调用时的环境。访问独立变量函数能访问不属于自己作用域的变量。定义时而非调用时这就是 JavaScript 的词法作用域规则。闭包 函数 函数定义时所在的词法环境本质函数能记住并访问创建时的词法作用域即使在作用域外执行依然可以访问外部变量这就是闭包。1.1 词法作用域Lexical ScopingJavaScript 的作用域是“静态”的。函数在定义的那一刻就确定了它能访问哪些变量跟在哪里调用无关。 函数的作用域由其在代码中的书写位置决定定义时。functionouter(){constnameOuter;// inner 函数就是闭包// inner函数形成了对outer函数中name变量的闭包functioninner(){console.log(name);// 访问外部函数的变量}returninner;}constclosureFuncouter();closureFunc();// 输出: Outer在上面的例子中inner函数在 outer函数内部定义它就“记住”了 outer的作用域。即便 outer执行完了inner依然能访问 name。二、 底层揭秘V8 引擎眼中的闭包2.1 关键角色[[Environment]]在 JS 引擎如 V8中每个函数都有一个内部隐藏属性[[Environment]]。[[Environment]] 是函数在创建时保存的一个引用指向其外层的词法环境Lexical Environment。当函数执行时会创建一个执行上下文Execution Context其中包含词法环境Lexical Environment存储变量 (let/const/function)函数定义时不是调用时会通过内部属性 [[Environment]] 保存当前词法环境的引用 。变量环境Variable Environment存储 varouter 指针指向上一层词法环境形成作用域链全局环境 ← outer 环境 ← inner 环境outer 执行完毕执行上下文弹出调用栈但 inner 的 [[Environment]] 仍引用 outer 的词法环境垃圾回收机制GC不会回收该环境变量会被保留不会销毁2.2 闭包的形成过程我们用一段经典代码来拆解functioncreateCounter(){letcount0;// 被闭包“捕获”returnfunction(){count;returncount;};}constcountercreateCounter();counter();Step 1函数定义阶段createCounter 定义内部匿名函数定义时它的 [[Environment]] 指向 createCounter 的词法环境。Step 2执行 createCounter()创建 createCounter 执行上下文创建词法环境包含 count 0返回内部匿名函数匿名函数带着 [[Environment]] 一起被返回Step 3createCounter执行完毕正常情况下函数执行完执行上下文出栈局部变量销毁但闭包阻止了销毁因为匿名函数的 [[Environment]] 仍在引用 countGC(V8 的垃圾回收器) 判定变量可达 → 词法环境被保留Step 4执行 counter()匿名函数执行在自身作用域找不到 count沿着 [[Environment]] 找到保留的词法环境读取并修改 count✅ 结论闭包在 V8 中本质上是被函数捕获并延长生命周期的词法环境。三、 作用域链 闭包Global Context (全局执行上下文) ├── LexicalEnvironment (词法环境) │ ├── createCounter: function │ └── counter: function (指向 inner (内部)函数) │ └── createCounter 执行后 (环境未被销毁!) ├── LexicalEnvironment (词法环境 依然存活) │ ├── count 1 (被修改后的值) │ └── inner: function (内部函数) │ └── [[Environment]] ──────────────┐ │ counter() 执行时 ▼ inner Execution Context(内部函数执行上下文) ├── LexicalEnvironment (自身词法环境) └── Outer Reference (作用域链) ──► createCounter 的 LexicalEnvironment作用域链本质沿着 [[Environment]] 一层层向上查找变量。闭包本质让外层作用域不会被回收。四、 闭包的经典应用场景4.1 模块模式Module Pattern这是早期 JS 实现私有变量的唯一手段。constmyModule(function(){letprivateVar我是私有的;// 外部无法直接访问functionprivateMethod(){console.log(privateVar);}return{publicMethod:function(){privateMethod();}};})();myModule.publicMethod();// 我是私有的// myModule.privateVar; // undefined// myModule.privateMethod(); // 报错4.2 函数工厂 柯里化Currying// 函数柯里化// 1. 定义外层函数 multiply(a),// 接收第一个参数暂存起来functionmultiply(a){// 2. 返回新函数形成闭包// 内部函数会记住外层作用域的 areturnfunction(b){returna*b;};}// 3. 执行 multiply(2)// a 2 被闭包保留不会销毁// 返回的函数赋值给 doubleconstdoublemultiply(2);// 4. 执行 double(5)// 使用闭包中保存的 a2double(5);// 10柯里化本质把多参函数拆成一系列单参函数利用闭包记住前面的参数。好处参数复用、逻辑复用、批量生成工具函数。4.3 防抖与节流Debounce Throttle前端性能优化高频方案闭包用于保存定时器 / 时间戳。// 防抖函数示例// 防抖延迟 delay 后执行若期间再次触发则重新计时functiondebounce(fn,delay){// 闭包保存定时器 ID避免多次触发重复执行lettimernull;returnfunction(...args){// 清除之前的定时器clearTimeout(timer);// 重新设置新定时器timersetTimeout((){fn.apply(this,args);},delay);};}// 使用示例consthandleInputdebounce(function(){console.log(发送搜索请求);},500);// 输入框频繁输入时只会在停止输入 500ms 后执行一次input.addEventListener(input,handleInput);// 节流Throttle// 核心思想在 指定时间内函数只执行一次不管触发多频繁都按固定频率执行。// 常用于滚动监听、窗口 resize、高频点击、鼠标移动等。functionthrottle(fn,interval){// 用闭包保存上一次执行的时间戳letlastTime0;returnfunction(...args){constnowDate.now();// 判断当前时间 - 上次执行时间 间隔时间if(now-lastTimeinterval){fn.apply(this,args);lastTimenow;// 更新最后执行时间}};}// 使用示例滚动节流consthandleScrollthrottle(function(){console.log(滚动触发了,scrollY);},300);window.addEventListener(scroll,handleScroll);防抖等待一段时间只执行最后一次节流固定时间内只执行一次两者都必须依靠闭包保存状态timer /lastTime否则无法实现。五、 踩坑指南闭包的内存陷阱闭包虽好但不能滥用。5.1 循环中的经典坑// 错误示例for(vari1;i5;i){setTimeout(function(){console.log(i);// 全部输出 6},i*1000);}// 原因var没有块级作用域i是全局变量所有回调函数共享同一个 i。✅ 解决方案1IIFE(立即调用函数表达式用于创建独立闭包作用域。)for(vari1;i5;i){(function(j){setTimeout(function(){console.log(j);// 1, 2, 3, 4, 5},j*1000);})(i);}✅ 解决方案2推荐let块级作用域for(leti1;i5;i){setTimeout(function(){console.log(i);// 1, 2, 3, 4, 5},i*1000);}(注let在循环中每一轮都会创建一个新的、独立的词法环境相当于自动闭包。)5.2 内存泄漏风险如果一个闭包持有巨大的数据结构且长期不释放会导致内存占用过高。functionheavyTask(){constbigDatanewArray(10000000);// 大对象returnfunction(){console.log(done);};}consttaskheavyTask();// 即使 heavyTask 执行完了bigData 依然存在// 内存不释放 → 页面卡顿tasknull;// 手动切断引用让 GC 回收JavaScript 闭包保留的是「整个词法环境」不是「用到的变量」。只要返回内部函数 → 形成闭包 → 外层所有变量都会被保留。无论内部函数有没有使用都不会被回收。5.3过多闭包可能导致性能问题闭包变量需要沿作用域链查找比局部变量稍慢。大量长期驻留的闭包会增加内存压力注意及时释放。六、 总结闭包的本质函数 定义时的词法环境 的结合体。形成原理基于词法作用域函数通过 [[Environment]] 引用外部环境。V8 视角闭包是未被垃圾回收的词法环境。核心价值数据封装、私有变量、状态持久化。注意事项警惕循环陷阱、内存占用不用时及时置 null 释放。