闭包(Closure):JavaScript 里的 “记忆小助手”

闭包(Closure):JavaScript 里的 “记忆小助手”
Touko闭包是 JavaScript 中比较容易让人困惑的概念 —— 它能让函数 “记住自己成长的环境”,哪怕后来跑到别的地方工作,也能找回当初的变量。今天就抛开复杂术语,用更通俗的方式拆解闭包,从原理到实战一次讲明白!
前置知识
如果对作用域链还不熟悉,建议先看下面这篇。理解函数如何查找变量后,再理解闭包会更顺畅~
一、闭包的本质:函数 + 诞生环境
1.1 到底什么是闭包(Closure)?
官方定义有点抽象:闭包是函数与其声明时所在词法环境的组合。
换成人话就是:一个函数在 “老家”(声明时的作用域)定义时,记下了周围的变量;后来就算跑到 “外地”(其他作用域)执行,也能找到这些 “老家的变量”—— 这就形成了闭包。
举个直观的例子,你看这个 “带记忆的函数”:
1 | function createMemory() { |
这里的getSecret就是闭包 —— 它记得自己 “老家” 的环境,所以就算createMemory执行完了,也能找回secret。
1.2 闭包形成的三个关键条件
不是所有函数都是闭包,得满足这三个条件:
- 函数嵌套:内部函数定义在外部函数里面(比如
getSecret在createMemory里) - 记着老家的变量:内部函数用到了外部函数的变量或参数(
getSecret用了secret) - 离开老家:内部函数被返回、传递到外部函数作用域之外(
getSecret被 return 出去,赋值给myMemory)
这里有个容易误解的点: 闭包不是我们 “刻意创建” 的,而是函数定义时的自然结果!只要满足上面三个条件,闭包就会自动形成 —— 不用写特殊语法,它就在那了。
1.3 闭包的核心特点
- 变量持久化:相当于给变量 “延长了生命周期”。正常情况下,函数执行完,内部变量会被垃圾回收机制清理,但闭包能让变量一直 “活着”(比如上面的
secret) - 变量私有权:外部无法直接碰闭包记住的变量。比如你没法直接改
secret,只能通过getSecret这个 “接口” 访问 —— 这就实现了数据私有 - 作用域链保留:闭包不只是记着自己用的变量,而是记着整个 “老家的环境链”(外部函数作用域 → 全局作用域)。比如如果
secret还引用了全局变量,闭包也能找到
二、拆解闭包:看看它 “记着” 哪些东西
2.1 经典案例:带闭包的计数器
用闭包实现计数器是最常见的场景,能清晰看到闭包如何 “保管” 变量:
1 | function createCounter() { |
这里的increment、decrement、getValue都是闭包,它们共享同一个count变量 —— 不管调用哪个方法,操作的都是同一个计数器,就像几个人共用一个笔记本一样。
2.2 闭包的作用域链
闭包能找到变量,靠的是作用域链。拿上面的计数器举例,作用域链是这样的:
graph LR A["内部函数(increment)的环境"] --> B["外部函数(createCounter)的环境"] B --> C[全局环境]
当increment执行时,会先在自己的环境找count(没找到),再顺着作用域链往上找,直到在createCounter的环境里找到 —— 这就是闭包能访问外部变量的原理。
关键点:闭包会保留整个作用域链,而不仅仅是它用到的变量!
三、闭包的实战场景
闭包不是理论概念,实际开发中到处都有它的影子,分享几个我常用的场景:
3.1 数据封装:实现 “私有变量”
JavaScript 没有原生的 “私有变量” 语法,但用闭包就能模拟。比如实现一个银行账户,余额只能通过存款、取款接口操作,不能直接修改:
1 | function createBankAccount(initialBalance) { |
我在写组件状态管理时,经常用这种方式封装敏感数据,防止外部代码误改导致 bug。
3.2 函数工厂:批量生成 “定制化函数”
如果需要多个逻辑相似但参数不同的函数,用闭包做 “函数工厂” 特别方便。比如生成不同风格的问候语:
1 | function createGreeting(prefix) { |
我之前做表单验证时,用这个方法生成了一堆校验函数(比如 “最小长度校验”“最大长度校验”),不用重复写逻辑,代码简洁了很多。
3.3 事件处理:保留 “上下文信息”
写事件绑定的时候,经常需要在回调里用外部变量,这时候闭包就派上用场了。比如给多个按钮绑定点击事件,输出各自的索引:
1 | const buttons = document.querySelectorAll('button'); |
这里的箭头函数就是闭包,它记住了循环时的i。不过,如果用var声明i(没有块级作用域),反而会出问题,得用 IIFE + 闭包兼容,不过现在有let就简单多了。
3.4 模块模式:早期前端模块化的基础
在 ES6 Module 出现之前,前端模块化基本靠闭包实现。比如封装一个工具模块,只暴露需要的接口,内部逻辑隐藏起来:
1 | const myModule = (function () { |
这种模式我在维护老项目时经常见到,虽然现在有 ES Module 了,但闭包的封装思想依然很有用。
四、闭包的坑:内存浪费与优化
闭包虽好用,但用不好容易出问题,最常见的就是 “内存浪费”(内存泄漏)。
4.1 为什么会内存浪费?
闭包会 “留住” 它记住的环境里的变量,不让垃圾回收机制(GC)清理。如果闭包引用了大对象(比如 DOM 元素、超大数组),又一直没被销毁,这些对象就会一直占着内存,导致内存越用越多。
举个反面例子:
1 | function heavyOperation() { |
如果这种闭包多了,页面会越来越卡,甚至崩溃。
内存泄漏警告: 闭包会阻止其作用域链上所有变量被回收,不当使用会导致内存泄漏!
4.2 优化技巧:避免内存浪费
分享几个我实战中用过的优化方法:
- 主动 “忘记” 变量:不用闭包时,手动把它记住的变量设为
null,让 GC 能清理
1 | function createClosure() { |
- 只记需要的内容:如果只需要大对象的部分数据,就别引用整个对象
1 | function createClosure() { |
4.3 闭包 vs ES6 块级作用域
有些场景下,ES6 的let/const(块级作用域)能替代闭包,代码更简洁。比如之前的循环绑定事件:
1 | // 旧方法:用IIFE+闭包兼容var的问题 |
因为let在循环里每次迭代都会创建一个新的作用域,定时器回调能直接拿到当前的i —— 这时候就不用再写闭包了。
五、底层原理:闭包为什么能 “记住” 环境?
其实闭包的底层就是 “作用域链” 的机制。再梳理下它的形成过程:
graph TD A["外部函数被调用,创建执行环境"] --> B["内部函数定义,记录当前的词法环境"] B --> C["内部函数引用外部函数的变量"] C --> D["外部函数执行完,返回内部函数到外部作用域"] D --> E["内部函数被调用时,通过作用域链找到外部变量,闭包生效"]
简单说:函数定义时会 “记下自己在哪出生”(词法作用域),执行时会顺着这个 “出生地链条” 找变量 —— 闭包就是利用了这个机制,让函数在别的地方执行时,还能找到 “老家” 的环境。
5.1 现代引擎的优化:不用怕闭包影响性能
很多人担心闭包性能差,但现代 JavaScript 引擎(比如 V8,Chrome 和 Node.js 用的)对闭包做了很多优化:
- 按需保留:只保留闭包真正用到的变量,没用的变量会被 GC 清理(比如上面的
largeData如果没被引用,就会被回收) - 缓存常用变量:频繁访问的闭包变量会被缓存,访问速度更快
- 优化对象查找:对闭包引用的对象做特殊处理,减少属性查找时间
所以不用过度担心闭包的性能问题 —— 正常使用下,性能开销可以忽略不计。只有在循环里频繁创建闭包(比如每秒创建上千个),才需要注意优化。
六、闭包最佳实践:这些坑我踩过,你别踩
分享几个我实战中总结的经验,帮你避开闭包的坑:
少记无关内容:只让闭包记住必要的变量,别把不需要的东西也带在身上。比如上面的银行账户,只暴露
depositMoney等方法,不暴露balance。能不用就不用:如果用块级作用域(
let/const)能解决问题,就别用闭包。比如循环里用let替代var + 闭包,代码更简洁,也少了内存开销。小心循环引用:闭包引用 DOM 元素时,容易形成 “闭包 →DOM→ 闭包” 的循环引用。解决方法是:事件不用时手动移除监听,或用
WeakMap/WeakSet(弱引用)存储 DOM。明确生命周期:用闭包封装组件时,尽量做成 “自包含” 的模块,不用时调用清理方法,释放内存。
七、常见问题解答
Q1:闭包是什么?举个例子
闭包是函数记住并访问其词法作用域的能力,哪怕函数在词法作用域之外执行。比如实现计数器:
1 | function createCounter() { |
Q2:闭包有什么优缺点?
优点:
- 实现数据封装(私有变量),防止外部误改
- 保留变量状态(比如计数器),不用依赖全局变量
- 批量生成定制化函数(函数工厂)
- 是早期前端模块化的基础
缺点:
- 可能导致内存浪费(引用的变量无法清理)
- 频繁创建闭包有轻微性能开销
- 调试时不容易追踪变量来源(Chrome DevTools 里变量会标为
Closure)
Q3:如何避免闭包导致的内存浪费?
- 只让闭包记住必要的变量,减少无关引用
- 不用闭包时,主动把引用的变量设为
null - 避免在闭包里引用大对象(比如大数组、整个 DOM 树)
- 用
WeakMap/WeakSet存储引用,让 GC 能清理 - 明确闭包的生命周期,不用时执行清理逻辑(比如移除事件监听)
八、闭包总结:不是难题,是实用工具
最后用一张表总结闭包的核心特性,方便大家回顾:
| 特性 | 说明 | 应用场景 |
|---|---|---|
| 词法作用域 | 闭包的基础,函数定义时决定 “老家” | 所有闭包场景 |
| 保留变量状态 | 让变量不被清理,持续可用 | 计数器、缓存工具 |
| 数据封装 | 变量私有,只能通过闭包访问 | 模块开发、模拟类 |
| 函数工厂 | 动态生成带定制参数的函数 | 表单验证、问候语生成 |
| 内存开销 | 保留作用域链,可能占内存 | 需要优化的场景 |
| 调试标识 | Chrome DevTools 中变量标为Closure |
调试闭包相关问题 |
很多人觉得闭包难,是因为它 “看不见摸不着”—— 但其实只要理解 “函数记住老家环境” 这个核心,再结合实际场景多练,很快就能掌握。
闭包不是需要害怕的难题,而是 JavaScript 里实用的工具。用好了它,你就能写出更安全、更优雅的代码,比如封装组件、管理状态、实现模块化 —— 现在就试着用闭包写个小工具吧!










