Skip to content

JavaScript 闭包深度解析:从概念到引擎实现

一、什么是闭包?

定义

闭包(Closure)是指:一个函数能够访问并记住其外层作用域中的变量,即使外层函数已经执行完毕

经典示例

js
function outer() {
  let x = 10;
  function inner() {
    console.log(x); // 可以访问 outer 的变量
  }
  return inner;
}

const fn = outer(); // outer 执行完毕
fn(); // 输出 10 —— 闭包生效

二、闭包的核心机制

闭包不是“魔法”,而是 JavaScript 引擎基于词法作用域引用机制的自然产物。其底层实现可分为四个关键步骤:

步骤 1️⃣:词法作用域确定访问权限

  • innerouter 内部定义 → 静态地“有权”访问 outer 的变量。
  • 这是静态的,在代码书写时就已决定。

步骤 2️⃣:函数创建时绑定 [[Environment]]

inner 被创建时,JS 引擎会设置其内部属性:

text
inner.[[Environment]] = outer 的词法环境(包含 x: 10)
  • [[Environment]] 是一个隐式引用,指向 inner 定义时所在的词法环境。
  • 这是闭包实现的关键。

步骤 3️⃣:外部函数执行完毕,执行上下文出栈

js
const fn = outer(); // outer 执行完毕,执行上下文从调用栈弹出
  • outer执行上下文被销毁。
  • outer词法环境(变量 x)并未被回收,因为 inner.[[Environment]] 仍在引用它。

步骤 4️⃣:内部函数调用时查找变量

js
fn(); // 实际调用的是 inner
  • 创建 fn 的执行上下文。
  • 查找 x
    • 本地没有 → 通过 [[Environment]] 找到 outer 的词法环境 → 找到 x = 10

注意:是 fn() 调用 inner,不是再次调用 outerouter 通常只执行一次。

三、执行上下文 vs Context 对象

概念类型说明
执行上下文(Execution Context)抽象概念JS 规范中用于管理函数执行的状态,包含 LexicalEnvironmentVariableEnvironmentThisBinding
Context 对象(V8 实现)堆内存对象V8 引擎为实现闭包而创建的真实对象,存储变量绑定(如 x: 10),可通过 [[Environment]] 被引用

关系
执行上下文是“逻辑模型”,Context 对象是它的“物理实现”。
outer 执行完毕后,执行上下文销毁,但 Context 对象仍存在于堆中,只要被闭包引用。

四、[[Environment]] 与指针语义

虽然 JavaScript 没有指针语法,但 [[Environment]] 的行为本质上就是受控的指针

  • 它存储的是内存地址(引用),而非值本身。
  • 多个闭包可共享同一个 Context 对象。
  • 形成作用域链(Scope Chain),类似指针链。
  • 支持间接访问(解引用)。

它是一种“安全的、自动管理的指针”,避免了野指针和内存泄漏,但保留了高效访问的能力。

五、垃圾回收如何处理闭包?

GC 算法:标记-清除(Mark-and-Sweep)

JS 引擎通过可达性分析判断哪些对象需要回收:

  1. 标记阶段:从根对象(全局对象、调用栈等)出发,遍历所有可到达的对象。
  2. 清除阶段:未被标记的对象被视为“不可达”,释放其内存。

闭包的生命周期

  • 只要 inner 存在,inner.[[Environment]] 就引用 Context_AContext_A 不会被回收。
  • fn = null 后,Context_A 无法从根到达 → 下次 GC 时被回收。

风险:若忘记断开引用(如未清理事件监听器),会导致内存泄漏。

六、闭包的风险:变量污染与共享陷阱

闭包通过引用共享变量,可能导致“意外状态共享”,即“变量污染”。

经典案例:循环中的 var

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 3 3 3
}
  • 所有回调共享同一个 i(函数级作用域)。
  • 循环结束后 i = 3,回调执行时都输出 3

解决方案

使用 let(推荐)

js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出 0 1 2
}
  • let 为每次迭代创建新的词法环境,每个闭包捕获独立的 i

使用 IIFE 制造隔离

js
for (var i = 0; i < 3; i++) {
  (function(i) {
    setTimeout(() => console.log(i), 100);
  })(i);
}

七、最佳实践与性能建议

建议说明
✅ 优先使用 const / let避免意外修改共享变量
✅ 避免不必要的闭包减少内存占用
✅ 主动断开引用fn = null,帮助 GC 回收
✅ 使用 WeakMap / WeakSet存储私有数据,不阻止 GC
✅ 监控内存使用使用 Chrome DevTools 分析内存泄漏

八、面试如何回答闭包?

初级回答

“内部函数可以访问外部函数的变量。”

中级回答

“即使外部函数执行完了,内部函数仍能访问其变量,因为作用域链还在。”

高级回答(推荐)

“闭包是函数与其创建时词法环境的绑定。JS 引擎通过 [[Environment]] 内部属性实现这一机制。 当外部函数执行完毕,其执行上下文出栈,但只要内部函数存在,它就会通过 [[Environment]] 保持对外层变量的引用,防止被 GC 回收。 后续调用内部函数时,变量查找沿作用域链完成。一旦内部函数也被释放,GC 才会回收这些变量。”

结语

闭包是 JavaScript 最强大、也最容易被误解的特性之一。
理解它,不仅是“会写代码”,更是理解 JS 引擎如何管理作用域、内存、执行上下文的钥匙。

当你能清晰区分:

  • 执行上下文 与 Context 对象
  • [[Environment]] 与 指针语义
  • 共享引用 与 变量污染

你就真正掌握了 JavaScript 的核心机制。