如何通过内存快照(Heap Snapshot)定位闭包中不可见的 Retainers的重点在于把前置条件、操作顺序和容易误判的地方分清楚。
Chrome DevTools Retainers面板不显示闭包变量,因其通过[[Scopes]]隐式持有而非显式属性;需在Heap Snapshot中筛选函数对象,展开[[Scopes]]→Closure查看实际捕获变量。
因为闭包变量本身不直接作为对象的显式属性存在,而是被函数作用域隐式持有——DevTools 的默认 Retainers 树只展示直接引用路径(比如 obj.prop → closure),而闭包内部的自由变量(free variables)是通过 [[Scopes]] 隐式关联的,不会出现在 Retainers 列表中。你看到的“(closure)”节点,往往只是个占位符,点不开、查不到具体持有哪些变量。
关键不是看 Retainers 面板,而是切换到 Containment 或 Objects 视图,再结合 Retained Size 和 Distance 定位可疑函数对象,然后逐层展开其 [[Scopes]]:
Heap Snapshot 中筛选类型为 (closure) 或 Function 的对象,按 Retained Size 降序排列,优先检查那些 retained size 异常大但自身 size 很小的函数Properties → 展开 [[Scopes]] → 找到 Closure 类型的 scope 对象Closure scope → 查看它的 Properties,里面就是实际被捕获的变量(如 data、config、timerId 等),这些才是真正的内存持有者Reveal in Summary view,回到 Summary 视图查它的 Retainers,确认是谁让这个闭包没被释放以下场景下,闭包的 Retainers 极易被忽略,因为引用链不直观:
setTimeout/setInterval 回调中捕获了外部大数组或 DOM 节点,但定时器未清除 → 函数对象一直存活,[[Scopes]] 中的变量持续被保留removeEventListener 解绑 → 即使 DOM 节点已移除,只要监听器还在,闭包及其捕获的上下文就无法 GCuseCallback 返回的函数捕获了过重的 props 或 state,且该函数被传给子组件并被缓存(如 memo 子组件依赖它)→ 闭包 retain 整个旧 render 的 contextPromise.then(closure)),closure 捕获了上层作用域的大对象,而 Promise 处于 pending 状态 → 闭包和所有捕获变量都被挂起保留不能只看对象是否“消失”,要确认 Retained Size 是否归零,且其 [[Scopes]] 下不再有活跃引用:
Collect garbage)Comparison 视图中筛选 Added Objects,查找新增的 (closure) 或未释放的 Closure scopeDistance 值:如果某个 closure 的 Distance 是 1,说明它被全局或长期存活对象(如 window、document、模块级变量)直接持有,基本就是泄漏源闭包的 Retainers 不在表面,在 [[Scopes]] 里;而 [[Scopes]] 不会自动展开,得你一层层点进去看——这点最容易被跳过。