闭包——它延长了作用域的寿命,让变量在“该死”的时候还活着,创造出强大的封装和状态持久

29 阅读4分钟

真是拿你没办法……(扶额)。哪怕是沙耶香那个笨蛋,教到现在也该懂了。

你之所以还没懂,是因为你还在把代码当成“一行行读过去的文字”,而不是**“内存里的空间变化”**。

好,把之前的比喻全忘掉。我们要进手术室了。把脑子切开,我们要看**内存(Memory)**到底发生了什么。

不管你是高中学历还是博士,只要你玩过 RPG 游戏,下面这个逻辑你绝对能懂。


核心概念:函数是一个“临时副本”

想象你在玩《Dying Light》(消逝的光芒)。

  1. 普通变量:是你背包里的东西。
  2. 函数执行:是一个**“剧情副本”**。

通常情况(没有闭包):

你进副本 -> 打怪 -> 捡东西 -> 副本通关,地图关闭。

副本里掉在地上的所有装备,只要你没捡起来,副本关闭的一瞬间,全 部 销 毁。

闭包情况:

你进副本 -> 捡到一个“对讲机”(内部函数) -> 你把对讲机带出了副本。

重点来了:这个对讲机,连着副本里的指挥中心。

只要你手里还拿着这个对讲机,游戏系统就不能销毁那个副本里的指挥中心,因为你随时可能通过对讲机跟里面说话!


Step-by-Step 极慢速解剖

我们用最简单的代码,一行一行看内存里发生了什么。

TypeScript

// 1. 定义外部函数(这是地图编辑器,还没开始玩)
function createCounter() {
  let count = 0; // 目标变量

  // 2. 定义内部函数(这是那个“对讲机”)
  function add() {
    count = count + 1;
    console.log("当前杀敌数:", count);
  }

  // 3. 把内部函数交出去(带出副本)
  return add;
}

// 4. 游戏开始
const myCounter = createCounter(); 
// 5. 使用闭包
myCounter(); 
myCounter();

Step 1: const myCounter = createCounter();

这时候,JS 引擎做了三件事:

  1. 开辟空间:在内存里划了一块地(叫它 Scope A),也就是“副本 A”。

  2. 初始化变量:在 Scope A 里放了一个箱子,标签叫 count,里面装了 0

  3. 制造内部函数:在 Scope A 里制造了 add 函数。

    • 关键点:这个 add 函数身上有一根无形的**“脐带”**,死死地拴在 Scope A 上。它记得:“我出生在这里,这里有我的 count 箱子。”
  4. 返回并赋值createCounter 结束了。它把 add 函数扔给了外部变量 myCounter

此时的状态:

  • createCounter 执行完毕。按理说 Scope A 该被垃圾回收车铲平了。
  • 但是! 垃圾回收车发现:外部的 myCounter 抓着 add,而 add 身上有根脐带拴着 Scope A
  • 结论:不能铲!Scope A 被强行保留了下来。

Step 2: 第一次执行 myCounter();

  1. 你调用了 myCounter(也就是那个 add)。
  2. add 说:“我要执行 count = count + 1。”
  3. add 在自己肚子里找 count?没找到。
  4. add 顺着那根脐带爬回了 Scope A
  5. 它在 Scope A 找到了那个箱子,把里面的 0 改成了 1

输出:当前杀敌数: 1

内存状态:Scope A 里的 count 现在是 1。

Step 3: 第二次执行 myCounter();

  1. 你又调用了 myCounter
  2. add 再次顺着脐带爬回 同一个 Scope A
  3. 它看到箱子里现在是 1
  4. 它把 1 改成了 2

输出:当前杀敌数: 2

内存状态:Scope A 里的 count 现在是 2。


为什么你会晕?(你的思维误区)

你可能以为:createCounter 结束后,count 这个变量就“死”了,或者每次调用都是新的。

错!

闭包的本质是“永生”:

只要 myCounter 这个变量还存在(你还没关掉页面),那个被捕获的 Scope A 就像个幽灵房间,一直悬浮在内存里,专门供 myCounter 读写。


验证你懂没懂:平行宇宙测试

如果我们再来一次,会发生什么?

TypeScript

const counterA = createCounter(); // 开启副本 A
const counterB = createCounter(); // 开启副本 B

问题counterAcounterB 会打架吗?

答案

  1. 执行第一行:系统开辟了 Scope A(副本 A),里面的 count 是 0。counterA 拿着通往 Scope A 的钥匙。
  2. 执行第二行:系统开辟了 全新的 Scope B(副本 B),里面的 count 也是 0。counterB 拿着通往 Scope B 的钥匙。

它们是完全隔离的。

  • counterA() -> Scope A 的 count 变成 1。
  • counterB() -> Scope B 的 count 变成 1。
  • counterA() -> Scope A 的 count 变成 2。

counterB 根本不知道 counterA 已经杀到 2 了,它还在玩自己的 1


最后的总结(不说人话版 -> 说人话版)

  • 官方定义:闭包是函数和声明该函数的词法环境的组合。

  • 傲娇翻译:

    闭包就是一个**“不愿意断奶的函数”**。

    哪怕妈妈(外部函数)已经走了,它还死死抓着妈妈肚子里的东西(变量)不放。

    结果就是,那个变量被迫一直活在内存里,直到这个巨婴函数死掉为止。

好了。

这是最底层的解释了。如果你告诉我你懂了,下一步我会让你用这个原理去写一个“模块化”的功能。

如果你还不懂……

(叹气)

把这一段复制下来,去画在纸上。画两个框,连一根线。别光看着屏幕发呆!