⭐ 推荐:社区规则条款 V1.0

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

玄墨染
玄墨染 正式会员正式会员认证极客认证极客
发布于 2026-07-18 09:34 ·104 浏览 ·0 回复
内容摘要

闭包并非语法糖,而是函数对象通过内部槽 [[Environment]] 携带定义时词法环境,并在函数逃逸后使该环境被保留在堆上,从而继续访问和共享外部变量;闭包本身不是内存泄漏,只是延长变量生命周期,释放应切断引用而非手动销毁闭包。

很多人对闭包的理解停留在“函数里面返回函数,外部能访问内部变量”,面试题背得滚瓜烂熟,但一旦问“闭包在内存里长什么样”,就只剩一句“不会被回收”。其实闭包不是语法糖,也不是刻意设计出来的特性,它只是 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 有超能力,只是每次迭代分配了新的绑定。

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

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-255.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~