JavaScript 闭包全解析:作用域、自由变量与内存管理

136 阅读4分钟

闭包是 JavaScript 的核心难点之一,也是前端面试中的高频考点。要是没搞懂闭包,就很难真正吃透 JS 的作用域、内存管理,甚至函数式编程等核心概念。本文会用简单易懂的语言和一些典型的示例,帮你把闭包从原理到常见用法、易踩的坑都梳理一遍。


一、先搞清楚作用域和作用域链

理解闭包之前,先要把作用域和作用域链捋顺。

var n = 999;

function f1() {
    // 没有用 var/let/const 声明,会直接挂到全局
    b = 123;

    {
        // 块级作用域(ES6)
        let a = 1;
    }

    console.log(n); // 999
}

f1();
console.log(b); // 123
  • n 是全局变量,函数内部可以正常访问;
  • alet 定义,只在块级作用域里有效;
  • b 没有使用声明关键字,执行后就成了全局变量(相当于 window.b)。

二、闭包到底是什么?

📌 简单来说:

闭包就是函数和它引用的外部变量(自由变量)组合起来的一种结构。

只要一个函数在自己定义的作用域之外被调用,依然能访问它当初定义时的作用域里的变量,这就是闭包。


三、闭包经典示例:自由变量不会被销毁

示例 1:返回函数形成闭包

function f1() {
    var n = 999; // 自由变量

    function f2() {
        console.log(n);
    }

    return f2;
}

var result = f1(); // f1 已执行完
result(); // 999

从执行顺序看,f1 已经跑完了,按理说局部变量 n 应该销毁了。但因为 f2 还在使用 n,所以它被一直保留在内存里。这就是闭包最直观的表现。

💡 这段示例就证明了:被内部函数引用的自由变量,不会被销毁,而是一直存在,直到引用消失


示例 2:用闭包做数据封装

function f1() {
    var n = 999;

    // 提供修改接口
    nAdd = function () {
        n += 1;
    }

    function f2() {
        console.log(n);
    }

    return f2;
}

var result = f1();
result(); // 999
nAdd();   // 修改 n
result(); // 1000

这里我们把 n 封装在函数作用域里,只能通过 nAddf2 来读写。这种写法经常被用来做“私有变量”。

✅ 同样可以看到:nf1 的局部变量,但由于闭包存在,它不会被销毁,一直保留在内存里供后续使用。


四、this 与闭包:上下文易错点

闭包里用 this 很容易踩坑。下面这段代码就挺典型。


var name = "The Window";

var object = {
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name;
    };
  },
};

var fn = object.getNameFunc();
console.log(fn()); // 输出:My Object

如果你直接写 return this.name,那么内部返回的函数单独调用时 this 会指向全局对象,结果输出 The Window 而不是 My Object。这里通过 var that = this 把当前上下文保存下来,再用闭包访问 that,就能保证拿到正确值。

💡 这里也能看到闭包的特性:getNameFunc 执行完后,that 依然存在,因为被返回的函数引用了它。


五、闭包的几个典型用途

1. 读写函数内部变量(做私有化)

function counter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const c = counter();
c(); // 1
c(); // 2

像这种计数器,外面没法直接操作 count,只能通过闭包函数来访问,实现了简单的数据私有化。


2. 延迟执行、模块模式等

闭包也是很多高级用法的核心,比如:

  • 给事件处理函数传递额外上下文
  • 模块化封装(IIFE)
  • 函数柯里化、高阶函数、缓存等

六、闭包的注意事项 ⚠️

1. 容易导致内存泄漏

function f1() {
    var largeObj = new Array(1000000).fill('💾');

    return function () {
        console.log(largeObj[0]);
    }
}

这里闭包函数引用了 largeObj,所以 largeObj 会常驻内存。如果这个变量不再需要,最好手动断开引用:

largeObj = null;

2. 修改外部变量带来的副作用

function outer() {
    var count = 0;
    return {
        inc() { count++ },
        log() { console.log(count) }
    }
}
const c = outer();
c.inc();
c.log(); // 1

闭包可以修改父作用域的变量,这种可变性有时候会带来不可预期的结果。写的时候要留意状态的管理。


七、小结

特性解释
访问外部变量内部函数可以访问定义时父作用域的变量
延长变量生命周期被引用的变量不会立即销毁,直到所有引用消失
实现封装可以用来模拟私有变量
可能导致内存泄漏若引用没释放,变量一直存在,需手动清理

✍️ 一句话总结闭包:

闭包是连接函数内部和外部变量的桥梁,让局部变量“自由”地活得更久。


📌 结尾

如果这篇文章对你有帮助,记得点赞、收藏或评论,后面我还会更新更多 JavaScript 进阶干货,咱们一块把 JS 基础打牢!