《闭包:一个函数偷偷带走了我家的糖》—— 零基础也能懂的JS闭包

闭包不是魔法,是作用域链的必然结果

很多和我一样的初学者在一开始学习闭包(Closure)的时候觉得是JS的某种特异功能。但是实际上,闭包在ECMAScript 规范中是一个自然产物。

要彻底理解闭包,我们必须拆解 V8 引擎在执行代码的时候的底层逻辑:调用栈(call stack)执行上下文(execution context)以及词法环境 (lexical environment)中outer的引用

一. 执行上下文与 outer

在 JavaScript 中,每当一个函数被调用,引擎就会为它创建一份执行上下文(Execution Context)并压入调用栈 。 每个执行上下文中,都包含一个词法环境(Lexical Environment)。这个环境内部有两个重要组成部分:

  1. 环境记录(Environment Record):存放当前函数内部声明的变量和函数。
  2. 外部环境引用(outer) :指向它在词法上(写代码的位置)的外层执行上下文 。 正是这个 outer 引用,构成了我们常说的作用域链(Scope Chain)。当引擎在当前函数的环境中找不到某个变量时,就会顺着 outer 指向的外部环境一路向上查找,直到全局环境。

底层铁律: outer 的指向,在函数'定义(声明)'的时候就已经决定了,而不是在函数执行(调用)的时候决定。这就是"词法作用域"。

二.从内存视角拆解一个标准闭包

我们用一段最经典的闭包代码,来看看当它被 V8 引擎执行时,内存和调用栈里究竟发生了什么:

js 复制代码
function createCounter() {
  let count = 0;
  function change() {
    count++;
    console.log(count);
  }
  return change;
}

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

1. 执行 createCounter() 时

  • createCounter 的执行上下文被压入调用栈。
  • 它的词法环境中,变量 count 被初始化为 0,同时定义了函数 change。
  • 注意:此时 change 函数作为一个对象被创建,由于它在源码里写在 createCounter 内部,V8 引擎在创建它时,会赋予它一个隐藏属性 \[Scopes],这个属性会保持对当前 createCounter 词法环境的引用

2. createCounter() 执行完毕并返回时

  • 按照常规逻辑,一个函数执行完,它的执行上下文就会从调用栈弹出并销毁,释放内存。
  • 但是! 它的内部函数 change 被返回了,并被全局变量 counter 引用。
  • 因为 counter(即 change)还活着,而 counter 的 \[Scopes] 属性死死勾住了 createCounter 的词法环境。

3. V8 引擎的破例:Closure 对象的诞生

V8 发现 createCounter 虽然退栈了,但它里面的 count 变量还在被内部函数引用着。于是,垃圾回收机制(GC)不会清理这段内存。 V8 会把 change 函数用到的外部变量(这里是 count)打包,在堆内存(Heap)中创建一个专门的对象,这个对象就叫 Closure(闭包)

三. 调用闭包函数时的 outer 查找规则

现在,我们执行 counter()(即调用 change 函数):

  1. V8 创建 change 的执行上下文,压入调用栈。
  2. 此时,change 的词法环境被创建,它的 outer 引用指向哪里?
  • 指向它出生时的那个外层环境(即保留在堆内存中的 createCounter 的 Closure 空间)
  1. 执行 count++:
  • 引擎先在 change 本地环境中找 count,没找到。
  • 顺着 outer 链条,进入 createCounter 的闭包环境,找到了 count,将其修改为 1。 当 counter() 执行完,change 的上下文弹栈销毁,但那个堆内存中的 Closure 闭包空间依然存在。下一次你再调用 counter(),它依然顺着 outer 找到同一个 count 变量,实现累加。

四.为什么要从底层理解闭包?

如果只停留在比喻层面,你很难解释下面这两个高级前端面试必考的"深水区"问题:

1. 内存泄漏的本质是什么?

如果闭包函数(如上面的 counter)一直存活在全局作用域中(没有被置为 null),那么它通过 outer 间接引用的整条作用域链上的变量都无法被垃圾回收。这相当于在堆内存里钉死了一块空间,用得多了就会导致内存泄漏

2. V8 引擎的闭包优化

现代 V8 引擎非常智能。如果外层函数有一百个变量,但内部函数只用到了一个,V8 只会把用到的那个变量放进 Closure 对象中,其余没用到的变量在父函数弹栈时依然会被无情销毁。这种精细化的内存控制,只有理解了底层原理才能真正体会。

闭包是语言设计的必然

闭包不是动态注入的补丁,它是 "函数作为一等公民(First-class Function)""词法作用域(Lexical Scope)" 碰撞后的必然产物。 只要 JavaScript 允许函数作为返回值,且作用域由书写位置决定,那么通过 outer 引用将父级环境锁死在堆内存中的"闭包机制",就是维持程序逻辑正确的唯一解。

相关推荐
程序员黑豆2 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC3 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea3 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望3 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少4 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波0074 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis5 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝6 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
计算机魔术师6 小时前
新兴多智能体系统的模式与问题
前端
用户059540174467 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css