前端-闭包
闭包(Closure)是JavaScript中一个非常重要且强大的概念,理解闭包对于掌握JavaScript高级特性至关重要。下面我将从多个角度详细解释闭包的概念、原理和应用。
一、什么是闭包?
闭包是指那些能够访问独立 (自由) 变量的函数,或者说函数定义时的词法环境(lexical environment)而非调用时的环境。
访问独立变量:函数能访问不属于自己作用域的变量。
定义时而非调用时:这就是 JavaScript 的词法作用域规则。
闭包 = 函数 + 函数定义时所在的词法环境
本质:函数能记住并访问创建时的词法作用域,即使在作用域外执行,依然可以访问外部变量,这就是闭包。
1.1 词法作用域(Lexical Scoping)
JavaScript 的作用域是“静态”的。
函数在定义的那一刻,就确定了它能访问哪些变量,跟在哪里调用无关。
=> 函数的作用域由其在代码中的书写位置决定(定义时)。
functionouter(){constname='Outer';// inner 函数就是闭包// inner函数形成了对outer函数中name变量的闭包functioninner(){console.log(name);// 访问外部函数的变量}returninner;}constclosureFunc=outer();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):存储 var
- outer 指针:指向上一层词法环境,形成作用域链
全局环境 ← outer 环境 ← inner 环境- outer 执行完毕,执行上下文弹出调用栈
- 但 inner 的 [[Environment]] 仍引用 outer 的词法环境
- 垃圾回收机制(GC)不会回收该环境
- 变量会被保留,不会销毁
2.2 闭包的形成过程
我们用一段经典代码来拆解:
functioncreateCounter(){letcount=0;// 被闭包“捕获”returnfunction(){count++;returncount;};}constcounter=createCounter();counter();Step 1:函数定义阶段
createCounter 定义,内部匿名函数定义时,它的 [[Environment]] 指向 createCounter 的词法环境。
Step 2:执行 createCounter()
- 创建 createCounter 执行上下文
- 创建词法环境,包含 count = 0
- 返回内部匿名函数
- 匿名函数带着 [[Environment]] 一起被返回
Step 3:createCounter执行完毕
- 正常情况下,函数执行完,执行上下文出栈,局部变量销毁
- 但闭包阻止了销毁
- 因为匿名函数的 [[Environment]] 仍在引用 count
- GC(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 被闭包保留,不会销毁// 返回的函数赋值给 doubleconstdouble=multiply(2);// 4. 执行 double(5)// 使用闭包中保存的 a=2double(5);// 10柯里化本质:
把多参函数拆成一系列单参函数,利用闭包记住前面的参数。
好处:参数复用、逻辑复用、批量生成工具函数。
4.3 防抖与节流(Debounce & Throttle)
前端性能优化高频方案,闭包用于保存定时器 / 时间戳。
// 防抖函数示例// 防抖:延迟 delay 后执行,若期间再次触发则重新计时functiondebounce(fn,delay){// 闭包保存定时器 ID,避免多次触发重复执行lettimer=null;returnfunction(...args){// 清除之前的定时器clearTimeout(timer);// 重新设置新定时器timer=setTimeout(()=>{fn.apply(this,args);},delay);};}// 使用示例consthandleInput=debounce(function(){console.log('发送搜索请求');},500);// 输入框频繁输入时,只会在停止输入 500ms 后执行一次input.addEventListener('input',handleInput);// 节流(Throttle)// 核心思想:在 指定时间内,函数只执行一次,不管触发多频繁,都按固定频率执行。// 常用于:滚动监听、窗口 resize、高频点击、鼠标移动等。functionthrottle(fn,interval){// 用闭包保存上一次执行的时间戳letlastTime=0;returnfunction(...args){constnow=Date.now();// 判断当前时间 - 上次执行时间 >= 间隔时间if(now-lastTime>=interval){fn.apply(this,args);lastTime=now;// 更新最后执行时间}};}// 使用示例:滚动节流consthandleScroll=throttle(function(){console.log('滚动触发了',scrollY);},300);window.addEventListener('scroll',handleScroll);防抖:等待一段时间,只执行最后一次
节流:固定时间内,只执行一次
两者都必须依靠闭包保存状态(timer /lastTime),否则无法实现。
五、 踩坑指南:闭包的内存陷阱
闭包虽好,但不能滥用。
5.1 循环中的经典坑
// 错误示例for(vari=1;i<=5;i++){setTimeout(function(){console.log(i);// 全部输出 6},i*1000);}// 原因:var没有块级作用域,i是全局变量,所有回调函数共享同一个 i。✅ 解决方案1:IIFE(立即调用函数表达式,用于创建独立闭包作用域。)for(vari=1;i<=5;i++){(function(j){setTimeout(function(){console.log(j);// 1, 2, 3, 4, 5},j*1000);})(i);}✅ 解决方案2(推荐):let块级作用域for(leti=1;i<=5;i++){setTimeout(function(){console.log(i);// 1, 2, 3, 4, 5},i*1000);}(注:let在循环中,每一轮都会创建一个新的、独立的词法环境,相当于自动闭包。)5.2 内存泄漏风险
如果一个闭包持有巨大的数据结构,且长期不释放,会导致内存占用过高。
functionheavyTask(){constbigData=newArray(10000000);// 大对象returnfunction(){console.log('done');};}consttask=heavyTask();// 即使 heavyTask 执行完了,bigData 依然存在// 内存不释放 → 页面卡顿task=null;// 手动切断引用,让 GC 回收JavaScript 闭包保留的是「整个词法环境」,不是「用到的变量」。
只要返回内部函数 → 形成闭包 → 外层所有变量都会被保留。
无论内部函数有没有使用,都不会被回收。
5.3过多闭包可能导致性能问题
闭包变量需要沿作用域链查找,比局部变量稍慢。
大量长期驻留的闭包会增加内存压力,注意及时释放。
六、 总结
闭包的本质:函数 + 定义时的词法环境 的结合体。
形成原理:基于词法作用域,函数通过 [[Environment]] 引用外部环境。
V8 视角:闭包是未被垃圾回收的词法环境。
核心价值:数据封装、私有变量、状态持久化。
注意事项:警惕循环陷阱、内存占用,不用时及时置 null 释放。
