前言
如果说 JavaScript 有什么概念是初学者觉得“如雾里看花”,那一定是闭包(Closure) 。闭包既是实现高级模式的基础,也是造成内存泄漏的诱因。本文将带你从底层原理出发,彻底攻克闭包。
一、 什么是闭包?
在 JavaScript 中,闭包最常见的表现形式是:在一个外部函数内创建了一个内部函数,内部函数引用了外部函数的局部变量,即使外部函数执行结束,这些变量依然存在。
二、 核心原理:为什么变量没被销毁?
1. 词法作用域规则
JavaScript 采用词法作用域(静态作用域),内部函数在定义时就记录了其所在的外层环境。
2. 内存驻留
通常,函数执行结束后,其执行上下文(Execution Context)会从调用栈中弹出,局部变量会被垃圾回收(GC)。 但如果内部函数被返回并在外部被引用,它会持有一个指向外部作用域的引用(Scopes),导致外部函数中被引用的变量无法被回收。
三、 闭包的实战用途
1. 封装私有变量(实现状态封装)
通过闭包,我们可以模仿类的私有属性,防止变量被外部随意修改。
function createCounter() {
let count = 0; // 私有变量
return {
increment() { count++; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
2. 缓存计算结果(Memoization)
利用闭包保存上一次计算的结果,避免耗时函数的重复计算。
3. 函数柯里化(Currying)
将接受多个参数的函数转化为一系列接受单个参数的函数。
四、 闭包的副作用:内存泄漏
闭包如果被过度使用或不当使用,会导致内存无法释放,进而引发内存泄漏。
1. 如何判断内存泄漏?
利用 Chrome DevTools 的 Performance 面板(或 Memory 面板):
- 录制一段时间的页面操作。
- 执行手动垃圾回收(点击小垃圾桶图标)。
- 判断标准:如果经过多次 GC 后,JS 堆内存的整体趋势曲线依然稳步向上,则说明存在内存泄漏。
2. 如何治理内存泄漏?
- 手动解除引用:在退出函数或不再需要时,将变量显式设为
null。 - 清除定时器:在使用
setInterval或setTimeout时,若回调引用了外部变量,务必在不需要时执行clearInterval。 - 减少全局变量:尽量使用模块化(ES Modules)或局部变量。
五、 面试模拟题(挑战一下)
Q1:闭包中的变量存储在栈中还是堆中?
参考回答: 通常基本类型存储在栈中,引用类型存储在堆中。但在闭包中,为了保证外部函数执行结束后内部函数仍能访问,这些被引用的变量会被**“提升”并存储在堆内存中**,直到内部函数被销毁。
Q2:看代码说结果,如何让其输出 0, 1, 2?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 结果:输出三个 3
参考回答: 这是因为 var 是函数作用域,三个定时器回调引用的是同一个 i。
- 解决方法 1:将
var改为let(利用块级作用域)。 - 解决方法 2:使用立即执行函数创建闭包,锁定每一轮的变量。
Q3:闭包一定会引起内存泄漏吗?
参考回答: 不一定。闭包引起的变量驻留是预期内的内存占用,只有当这些变量在逻辑上不再需要,但由于引用未断开导致 GC 无法回收时,才称为“内存泄漏”。合理使用闭包不仅安全,而且高效。
结语
闭包是 JavaScript 的精髓,它让函数拥有了“记忆力”。理解闭包,是迈向高级前端开发者的关键一步。