JS-深度理解 JavaScript 闭包及其内存陷阱

22 阅读3分钟

前言

如果说 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
  • 清除定时器:在使用 setIntervalsetTimeout 时,若回调引用了外部变量,务必在不需要时执行 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 的精髓,它让函数拥有了“记忆力”。理解闭包,是迈向高级前端开发者的关键一步。