Skip to content

闭包 (Closure) 深度解析

闭包是 JavaScript 中最强大但也最容易让人困惑的特性之一。它是理解高级 JS 编程、内存管理以及框架原理(如 Vue/React 响应式)的基石。


一、一句话先记住

闭包 = 函数 + 该函数能访问的外部变量。

或者更通俗地说:闭包让函数拥有了“记忆”,即使外部函数已经执行结束,它依然能记住并访问定义时的外部环境。


二、闭包的产生条件

当满足以下三个条件时,就产生了闭包:

  1. 存在嵌套函数:函数 A 内部定义了函数 B。
  2. 内部函数引用外部变量:函数 B 访问了函数 A 中的局部变量。
  3. 内部函数被传出:函数 B 被作为返回值,或赋值给了全局变量/定时器等。
javascript
function outer() {
  const name = "V8"; // 外部变量
  return function inner() {
    console.log(name); // 引用外部变量
  };
}

const closureFn = outer(); // outer 执行结束,但 name 变量被 inner 留住了
closureFn(); // 输出: "V8"

三、底层原理:作用域链 (Scope Chain)

在 JavaScript 中,每个函数在创建时都会关联一个 [[Scopes]] 属性。

  • inner 函数被定义时,它会记录父级函数 outer 的作用域。
  • outer 执行完毕弹出调用栈后,由于 closureFn(即 inner)依然存在且保留了对 outer 作用域的引用,导致 outer 的局部变量无法被销毁。

👉 关联知识


四、内存视角:闭包存哪了?

这是一个经典的面试题:闭包中的变量存放在栈里还是堆里?

1. 真相:存放在堆 (Heap) 中

通常情况下,函数的局部变量存放在**栈 (Stack)**中,函数执行完即销毁。 但 V8 引擎在解析代码时,如果发现内部函数引用了外部变量,它会做一个特殊处理:

  • 堆内存 中创建一个名为 Closure 的特殊对象。
  • 将被引用的变量从栈中移动到这个堆对象里。
  • 这样即使栈帧销毁了,内部函数依然可以通过指针在堆中找到这个变量。

👉 关联知识


五、闭包的常见应用场景

1. 模拟私有变量(模块化原型)

javascript
function createCounter() {
  let count = 0; // 私有变量
  return {
    add() { count++; },
    get() { return count; }
  };
}

const counter = createCounter();
counter.add();
console.log(counter.get()); // 1
console.log(counter.count); // undefined (无法直接访问)

2. 函数柯里化 (Currying)

javascript
function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplier(2);
console.log(double(5)); // 10

3. 异步回调与定时器

setTimeoutPromise.then 中,回调函数往往需要访问外部的上下文,这本质上都是利用了闭包。


六、闭包与内存泄漏

虽然闭包很有用,但如果使用不当,会导致内存无法及时回收。

1. 产生原因

只要闭包函数还被引用(如挂载在全局对象或 DOM 元素上),它所关联的外部变量就会一直常驻在 老生代堆内存 中。

2. 解决方案

手动解除引用:将闭包函数置为 null

javascript
let fn = outer();
// ... 使用闭包
fn = null; // 此时闭包引用的变量将被 GC 回收

👉 关联知识


七、面试高频题

1. 说说你对闭包的理解?

白话回答:闭包就是能读取其他函数内部变量的函数。在底层,它是函数和其词法环境的组合。它能让变量常驻内存,常用于封装私有变量、防抖节流等场景。

2. 闭包会造成内存泄漏吗?

白话回答:闭包本身不是泄漏,它是 JS 引擎的正常特性。只有当闭包被不当持有(比如长期挂载在全局),导致它引用的变量无法被 GC 回收时,才会产生内存泄漏。

3. 循环中定时器输出 i 的问题?

javascript
for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i); // 输出 6 6 6 6 6
  }, i * 1000);
}

原因var 没有块级作用域,定时器回调执行时 i 已经变成了 6。 闭包解法

javascript
for (let i = 1; i <= 5; i++) { // 使用 let 产生块级作用域闭包
  setTimeout(() => console.log(i), i * 1000);
}

八、总结

  • 优点:数据私有化、延长变量寿命。
  • 缺点:增加内存消耗、可能导致泄漏。
  • 核心心智模型:函数背着一个看不见的“包”(Scopes),包里装着它诞生时的环境数据。
最近更新