理解JavaScript闭包的内存模型,不背面试题也能讲清原理

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-12 08:51 ·2 浏览 ·0 回复

很多人对闭包的理解停留在“函数里面返回函数,外部能访问内部变量”,面试题背得滚瓜烂熟,但一旦问“闭包在内存里长什么样”,就只剩一句“不会被回收”。其实闭包不是语法糖,也不是刻意设计出来的特性,它只是 JS 词法作用域和执行模型自然产生的结果。把内存模型捋一遍,你会发现闭包根本不需要背。

闭包不是魔法,是函数带着它的出生环境

在 JS 里,函数是一等公民,它本身是一个对象。但和普通对象不同,函数对象有一个内部槽 `[[Environment]]`,指向它被创建时所在的词法环境。换句话说,每个函数都随身携带着“我是在哪定义的”这份记忆。

当这个函数在别的地方被调用时,JS 不会用调用位置的环境去解析变量,而是沿着 `[[Environment]]` 一步一步往外找。这就是词法作用域。如果函数引用了外部变量,并且这个函数逃逸到了定义它的作用域之外,那么它引用的那部分环境就不能被销毁——闭包由此产生。所以闭包不是“函数+内部变量”的固定搭配,而是“函数+它捕获的词法环境”。

从执行上下文看变量到底在哪

每次调用函数,JS 会创建一个执行上下文,压入调用栈。执行上下文里有一个词法环境,词法环境里又有环境记录,用来存放变量和函数声明。对于基本类型,值直接存在环境记录里;对于对象,环境记录里存的是堆内存中的引用。

调用结束后,执行上下文出栈,栈上的东西该清就清。但词法环境不一定跟着消失。如果没有任何函数引用它,垃圾回收器就会把它回收;如果有内部函数通过 `[[Environment]]` 指向它,它就会被保留在堆上,直到那个函数也不再被引用。这就是闭包内存模型的核心:环境记录被搬到了堆上,而不是留在栈里。

一个计数器例子,看内存怎么走

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

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

`createCounter` 被调用时,创建执行上下文,词法环境里有一个 `count = 0`。返回的内部函数在堆上创建,它的 `[[Environment]]` 指向 `createCounter` 的词法环境。`createCounter` 执行完毕,执行上下文出栈,但词法环境因为被内部函数引用,留在了堆内存中。

之后每次调用 `counter`,都会创建新的函数执行上下文。这个上下文的外层词法环境就是那个被保留的环境,所以变量查找能找到 `count`。`count++` 修改的是同一个环境记录里的值,因此计数会持续累加。

如果返回两个函数,它们会共享同一个词法环境:

function outer() {
  let x = 1;
  return {
    inc() { x++; },
    get() { return x; }
  };
}

`inc` 和 `get` 的 `[[Environment]]` 都指向同一个环境记录,所以一个改,另一个能看到。这也是闭包常被用来实现私有变量的原因。

为什么会有内存泄漏,又怎么释放

闭包本身不是内存泄漏,它只是延长了某些变量的生命周期。如果闭包长期存在,比如挂在 DOM 事件、定时器、全局变量上,那么它捕获的整个词法环境都会一直留在堆里。如果环境里引用了很大的对象,就可能造成实质上的内存占用。

释放的办法不是“手动销毁闭包”,而是切断引用。把保存闭包的变量置为 `null`,移除事件监听,清除定时器,闭包函数不再可达,垃圾回收器就会回收它以及它引用的环境。理解这一点,就不会再把“闭包导致内存泄漏”当成玄学。

循环里的 var 和 let,内存模型不同

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出 3 3 3

`var` 只有一个变量绑定,三个箭头函数的 `[[Environment]]` 都指向同一个环境记录,共享同一个 `i`。循环结束后 `i` 变成 3,所以都打印 3。

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出 0 1 2

`let` 在每次迭代时都会创建一个新的词法环境,每个箭头函数捕获的是各自迭代的环境记录,因此保留不同的 `i`。这不是 `let` 有超能力,只是每次迭代分配了新的绑定。

把闭包放回内存模型里看,它只是“函数对象持有对外层词法环境的引用”这一句话的具体展开。变量存在环境记录里,环境记录在堆上还是栈上,取决于有没有函数引用它。理解了这些,再讲闭包就不需要背面试题了。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-255.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~