news 2026/6/15 14:39:19

拒绝死记硬背!我是如何通过“内存引用图”彻底终结闭包困惑的?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拒绝死记硬背!我是如何通过“内存引用图”彻底终结闭包困惑的?

拨开迷雾:一次深入 JavaScript 闭包与内存模型的探索之旅

引言

JavaScript 中的闭包(Closure)是一个老生常谈的话题,但真正能从底层内存机制上将其彻底讲透的人并不多。在很长一段时间里,我对闭包的理解停留在“函数记住其外部变量”的表层概念上。每当遇到复杂的场景(如防抖节流、循环中的异步回调),我往往知其然而不知其所以然。

最近,通过一系列的深度剖析和自我诘问,我终于构建起了一个关于闭包、堆栈内存以及作用域链的清晰心智模型。本文旨在记录我从困惑到顿悟的整个思维演进过程,希望能帮助同样受困于此的开发者找到突破口。

阶段一:最初的误区与纠正——混淆“调用者”与“作用域”

故事始于一个经典的防抖函数实现。我想弄清楚为什么多次触发事件时,timer变量能够被共享。

JavaScript

function debounce(fn, t) { let timer; // 关键:这个变量为什么能被共享? return function() { if(timer) clearTimeout(timer); timer = setTimeout(fn, t); } } // debounce 函数只执行了一次 const handler = debounce(fn, 500); // 无论 handler 之后被谁调用,被调用多少次 box.addEventListener('mousemove', handler);

我的纠正认知:

我起初错误地将变量共享归因于调用者(this)相同。通过深入分析,我认识到“调用者是谁”与“作用域在哪”是完全独立的两个维度。timer之所以被共享,根本原因在于外部函数debounce(fn, 500)只执行了一次

它执行这一次,就在堆内存中创建了一个唯一的闭包环境(仓库),随后返回的函数始终持有这个唯一仓库的引用。

下面的流程图展示了初始化阶段和执行阶段的区别:

Code snippet

运行时阶段 (多次触发事件)
初始化阶段 (只执行一次)
堆内存 (Heap)
[[Environment]] 隐藏指针
永久锁定 Scope_1
通过携带的指针回溯访问
调用 handler 函数
浏览器触发 mousemove
浏览器再次触发 mousemove
执行 debounce(fn, 500)
创建并返回匿名函数
(即 handler)
创建唯一的词法环境对象
(Scope_1)
变量: timer

阶段二:深入内存模型——理解独立的闭包实例

解决了共享的问题后,新的疑问产生了:如果外部函数执行多次,产生的闭包是共享的还是独立的?

JavaScript

function fun() { let timer = 0; function test() { timer++; } return test; } // 两次独立的调用 const aa = fun(); const cc = fun();

深入内存层面的真相:

为了解答这个问题,我引入了堆(Heap)和栈(Stack)的内存模型。我意识到,必须将“函数的定义”和“函数的调用”区分开来。每次函数调用,都是一次全新的内存分配过程。

如下图所示,aacc虽然源自同一个工厂函数,但它们在内存中是两条完全平行的线:

Code snippet

堆内存(Heap) - 第二次调用 fun()
堆内存(Heap) - 第一次调用 fun()
栈内存(Stack)
[[Environment]]
指向
[[Environment]]
指向
引用地址
引用地址
函数对象 test_B
词法环境 Scope_B
timer: 0
函数对象 test_A
词法环境 Scope_A
timer: 0
变量 aa
变量 cc
  • const aa = fun():在堆中创建了一套全新的环境Scope_A和函数test_A

  • const cc = fun():在堆中又创建了另一套完全独立的环境Scope_B和函数test_B

  • 它们互不干扰,各自维护私有的状态。

阶段三:终极顿悟——“去中心化”的直连模型

在构建了内存模型后,我迎来了最大的思维障碍,也是理解闭包最关键的一步。

核心困惑与突破:

我曾潜意识地认为,子函数要访问父级变量,必须通过父函数的地址作为中介。我担心如果父函数执行完被销毁了,闭包链条会不会断裂。

最终的顿悟在于发现:闭包的连接是“去中心化”的直连,不需要父函数作为“中间商”。

const aa = fun()执行完毕,外部函数fun的执行上下文(Execution Context)从栈中弹出。虽然fun的作用域通常会被销毁,但因为返回的test函数(即aa)的[[Environment]]指针依然引用着这个词法环境,根据垃圾回收的可达性原则,这个环境必须被保留在堆内存中。

下图清晰地展示了这种错误的依赖关系与真实的直连关系之间的区别:

Code snippet

真实的内存模型(直连去中心化)
错误的理解模型(依赖父函数)
指向
错误地认为
需要通过父函数
再找到
指向
与闭包无关
已断开联系
[[Environment]]指针
直接锁定
子函数 test
栈变量 aa
词法环境 Scope
(独立堆对象,包含 timer)
父函数 fun
(已执行完毕,可被销毁)
子函数 test
栈变量 aa
父函数 fun
词法环境 Scope

总结我的最终理解模型:

  • 父函数只是一个“工厂”,负责创建环境和子函数。

  • 一旦子函数被创建,它就通过内部的[[Environment]]指针直接、独立地持有了对环境的引用。

  • 闭包的本质,就是返回的函数对象手中,持有一把直通其出生地(词法环境)的万能钥匙。这条链接与外部函数是否存活再无瓜葛。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 10:42:23

AI评测入门:零经验搞定标签分类

怎么设计提示词 我最近用 AI 实现了一个功能:基于标准标签体系,对某垂类产品评价内容自动打标签。 这是我第一次接触 AI 评测、第一次做标签分类、甚至第一次系统分析用户评价。 但正因“零经验”,反而让我更聚焦于最本质的问题:如何让 AI 输出符合预期? 有趣的是,虽…

作者头像 李华
网站建设 2026/6/15 10:43:40

EmotiVoice实战案例:为有声读物注入情感灵魂

EmotiVoice实战案例:为有声读物注入情感灵魂 在有声内容消费持续升温的今天,用户早已不再满足于“能听”的机械朗读。一段真正打动人心的旁白、一句饱含情绪的角色台词,往往能让听众瞬间沉浸其中——而这正是传统文本转语音(TTS&a…

作者头像 李华
网站建设 2026/6/12 23:38:19

从零到一:TikTok直播间系统化获客引流全攻略

TTSOP跨境互联 一站式提供TikTok账号 静态住宅IP,专为带货直播打造爆量通道。在TikTok商业化的浪潮中,直播间已从单纯的娱乐场,演变为品牌与用户深度连接、实现即时转化的核心战场。然而,许多创作者和商家常常面临这样的困境&…

作者头像 李华
网站建设 2026/6/15 14:17:43

咸鱼流出某大佬手搓爆改便携式一体机机箱,谁看谁迷糊,自带16寸高清大屏,真DIY大神骚操作,引5万人次浏览围观!

咸鱼上总是能够流出各种各样的电脑主机,有整机成品,也有如今被人吐槽已久的准系统,当然还有不少后配机箱的小主机,本身就是拆机主板流出,通过配备机箱外壳能够很好的发挥及匹配小主板。当然这些都是在本身硬件的基础上…

作者头像 李华
网站建设 2026/6/10 17:50:08

15、Linux文件服务器用户设置与工作站连接指南

Linux文件服务器用户设置与工作站连接指南 一、设置用户和组的配额 在Linux系统中,我们可以通过设置配额来管理用户和组对磁盘资源的使用。以下是具体的操作步骤: (一)设置用户配额 编辑配额文件 :假设我们要为用户 jkp 设置100MB的硬限制和110MB的软限制。首先,在…

作者头像 李华
网站建设 2026/6/14 5:58:01

19、连接 Windows 工作站到 Linux 域的全面指南

连接 Windows 工作站到 Linux 域的全面指南 在网络管理中,将 Windows 工作站连接到由 Linux 主域控制器(PDC)管理的域是一项常见任务。本文将详细介绍如何将 Windows XP Professional 工作站连接到该域,包括网络连接配置、域连接设置、漫游配置文件设置、共享目录连接以及…

作者头像 李华