《闭包:一个函数偷偷带走了我家的糖》—— 零基础也能懂的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 引用将父级环境锁死在堆内存中的"闭包机制",就是维持程序逻辑正确的唯一解。

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端