想象一下:你背着双肩包离开老家,里面放着一串钥匙。老家后来租给了别人,但只要有空你随时能掏出钥匙开门,因为钥匙上的齿纹和那把锁早已匹配好——哪怕你已经身在他乡。JavaScript 里的闭包,就是这把“随身携带的钥匙”:一个函数即使离开它诞生时的环境,依然能通过内部保存的引用,去访问外层函数的变量。这把钥匙让代码变得灵活、简洁,可若是永远不摘下来,背包就会越来越重,进程就会越来越慢——这就是内存泄漏。本文带你从闭包的底层原理出发,搞懂它的形成机制、常见用途、泄漏成因和排查技巧,像整理钥匙串一样,学会安全地使用闭包。

岗位关联

闭包是前端面试的必考点,也是写出优雅、低泄漏代码的核心能力。理解闭包与垃圾回收的关系,能让你在实际项目中既写出灵活的函数,又避免页面卡顿和内存暴涨。

目录
  1. 可视化演示
  2. 闭包概念
  3. 形成原理
  4. 应用场景
  5. 泄漏成因
  6. 危害与排查
  7. 防范策略
  8. 作用域链
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示JavaScript闭包三大核心:作用域链与变量捕获原理、定时器引用导致的内存泄漏成因、以及正确清理引用的防范策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

闭包形成原理 内层函数Fn通过作用域链捕获外层变量V1 即使外层函数Out已返回,V1仍被Fn引用 Fn → capture(Out.V1) → return Fn 闭包使变量生命周期超出函数作用域

闭包概念

你背着一个双肩包离开老家,包里常年装着一串钥匙。老家已经租出去了,但门锁没换——只要你想回去,随时可以掏出钥匙开门。JavaScript 里的闭包,正是这把“随身携带的钥匙”:一个定义在另一个函数内部的函数,即使外层函数执行完毕,它依然保留着对外层变量访问的能力。

在 JavaScript 中,函数可以嵌套定义,内层函数能看到外层函数声明的变量。这种由内向外可见的规则,叫做词法作用域。当一个内层函数被返回到外部使用时,它会连同自己当时所在的环境一起带走,形成一个密不可分的整体。这个整体,就是我们说的闭包。

要判断一段代码是否形成了闭包,可以看它是否同时满足两个条件:一是存在函数嵌套关系,二是内层函数引用了外层函数的变量。外层函数变量因为没有随函数调用结束而销毁,反而被内层函数“打包带走”,所以闭包本质上是一种把函数和环境绑在一起的机制。

这把“钥匙”的价值在于:它让函数可以携带自己的“私人物品”去任何地方使用。你给家里留了一扇永远能开的后门,这让程序设计拥有了极大的灵活性,也给数据封装提供了天然的手段。

闭包=函数+词法环境\displaystyle \text{闭包} = \text{函数} + \text{词法环境}
就像“钥匙 + 锁芯”才能开门一样,只有函数体而没有环境引用,不构成闭包;只有环境而没有被内层函数引用,也不会产生闭包。两者缺一不可。
  1. 函数体这是闭包中的“可执行部分”,决定了每一次调用要做什么事。它可以是返回值的函数、改数据的函数,也可以是传给事件监听的回调函数。
    函数: 可执行的指令集合\displaystyle \text{函数}:\ \text{可执行的指令集合}
  2. 词法环境这是函数创建那一刻所在的作用域快照,里面记录着所有可见变量和函数的引用。它不是变量的拷贝,而是变量地址的保存。
    词法环境: {变量→引用地址}\displaystyle \text{词法环境}:\ \{\text{变量} \to \text{引用地址}\}
  3. 结合的加法关系函数体与词法环境被绑定在一起,无论函数被传到何处,环境都如影随形。
    绑定: 函数⊕环境记录⇒闭包\displaystyle \text{绑定}:\ \text{函数} \oplus \text{环境记录} \Rightarrow \text{闭包}

形成原理

为什么钥匙离开老家以后依然有效?因为锁芯的型号、齿纹的规格,都被记录在了钥匙身上。程序里的闭包也一样:内层函数在创建时,把外层函数的“环境记录”像拍照一样存了下来,之后即使外层函数已经返回,这个快照仍然存在。

JavaScript 引擎在执行函数时,会维护一个叫做“作用域链”的结构。当你访问一个变量时,引擎先从当前函数自己的作用域找;找不到,就去外层函数的作用域找;再找不到,就继续往外,直到全局。这条链在函数定义时就被固定下来,不随调用位置改变。

当我们把内层函数 return 出去后,正常情况下外层函数的局部变量应该随调用结束而销毁。但因为内层函数的词法环境里引用着这些变量,引擎会把这些变量保留在堆内存中,而不是栈内存中。

引用保留是闭包形成的关键一步。正因为内层函数对外层变量一直“拉着不放”,外层变量才逃过了被回收的命运,得以在函数返回后继续生存,为闭包的各种应用提供基础。

查找(v)={v当前环境已定义向父层继续查否则\displaystyle \text{查找}(v) = \begin{cases} v & \text{当前环境已定义} \\ \text{向父层继续查} & \text{否则} \end{cases}
这就像你回老家时先看门口鞋柜(局部变量),没有就去客厅找(外层变量),再没有就去小区物业问(全局变量)。闭包的形成,让这个“找东西”的过程从函数调用结束的那一刻被延长到了闭包本身被销毁的那天。
  1. 当前环境检查内层函数访问变量时,先在自身参数和局部变量里找,命中就直接使用。
    当前环境∋v ⇒ 直接返回v\displaystyle \text{当前环境} \ni v \ \Rightarrow\ \text{直接返回} v
  2. 向父层回溯当前环境没有时,沿着作用域链向上走到外层函数的环境记录里继续找。
    父环境∋v ⇒ 返回v\displaystyle \text{父环境} \ni v \ \Rightarrow\ \text{返回} v
  3. 最终兜底父层也没有就一直找到全局环境,全局依然找不到则抛出 ReferenceError。
    全局环境∌v ⇒ ReferenceError\displaystyle \text{全局环境} \not\ni v \ \Rightarrow\ \text{ReferenceError}

应用场景

钥匙拿在手里,自然要开门办事。闭包最常见的用途,就是封装“只有你能进的空间”——模块化的私有数据。传统变量定义在模块内部,外部无法直接访问,但通过闭包暴露的接口函数,外部就能安全地操作这些私有数据,相当于只把钥匙给你信任的管家。

函数柯里化也是闭包的典型应用:把一个接收多个参数的函数,拆解成一系列只接收单个参数的函数。每拆一层,就通过闭包把前一个参数“记住”,等参数凑齐后再统一计算。这就像一个预约流程,每次预约记住一步,最后拿着完整订单去做事。计数器更是教科书级别的闭包应用:外层函数保存一个计数变量,内层函数每次调用就把它加一,因为闭包的存在,这个计数不会被重置。

闭包在事件回调中同样无处不在——按钮点击、网络请求、定时任务,回调函数总需要读取定义那一刻的数据,闭包保证了回调执行时这些数据依然可用。可以说,闭包是 JavaScript 模块化、函数式编程和事件驱动模型的基石。

count←count旧+1\displaystyle \text{count} \leftarrow \text{count}_{\text{旧}} + 1
计数器闭包能持久工作,正是因为 count 变量始终存活在词法环境中。如果不使用闭包,count 在每次函数调用结束后就会被销毁,计数就会归零。
  1. 读取旧值内层函数执行时,从词法环境里取出当前 count 的值。
    old←count\displaystyle \text{old} \gets \text{count}
  2. 加一运算把取出的值加 1,产生新数值。
    new←old+1\displaystyle \text{new} \gets \text{old} + 1
  3. 写回环境新值写回到闭包保存的变量中,下次调用继续沿用。
    count←new\displaystyle \text{count} \gets \text{new}

泄漏成因

钥匙多了不整理,背包就会越背越沉。闭包导致的泄漏,本质上就是你保留了一些不再需要的“钥匙”——不再使用的函数引用,却一直挂在全局变量、定时器或事件监听器上,导致引擎的内存管理器无法回收它们。

常见泄漏场景有三种:第一种是常驻引用,把一个本应短生命周期的闭包挂到全局变量上,全局变量永远活着,闭包就永远不会被释放;第二种是定时器未清理,setInterval 的回调里捕获了大型对象,即使页面早已不再需要这个定时器,它也依然每隔一段时间执行一次、占着内存;第三种是事件监听残留,DOM 元素被移除后,事件回调仍然绑定在其他对象上,闭包链里的数据就全都释放不掉。

循环引用更是老生常谈:对象 A 的闭包引用了对象 B,对象 B 又引用了对象 A,两个对象形成一个闭环,垃圾回收器即使知道它们都没用了,也因为彼此引用而难以判断是否该清除。这正是闭包使用不当带来的最典型“沉背包”。

泄漏=∑i=1n被闭包持有的对象i×未解除的引用\displaystyle \text{泄漏} = \sum_{i=1}^{n} \text{被闭包持有的对象}_{i} \times \text{未解除的引用}
每一项“被闭包持有的对象”一旦和“未解除的引用”相乘,就代表这些对象无法被垃圾回收器标记为可清除。求和符号表示多个这样的闭包叠加,内存占用就不断增大。
  1. 被闭包持有的对象闭包捕获了外层变量,变量指向的对象因此始终保持可达状态。
    对象i: 可达⇒不可回收\displaystyle \text{对象}_i:\ \text{可达} \Rightarrow \text{不可回收}
  2. 未解除的引用定时器没有 clear、事件没有 removeEventListener、全局引用没有置空,引用就一直悬挂着。
    引用≠∅⇒GC 无法回收\displaystyle \text{引用} \neq \varnothing \Rightarrow \text{GC 无法回收}
  3. 求和累积泄漏随闭包数量线性叠加,数量越大,内存损耗越明显。
    ∑i=1n→堆内存持续上升\displaystyle \sum_{i=1}^{n} \to \text{堆内存持续上升}

危害与排查

背包太重,走路就会变慢。内存泄漏最直接的表现就是内存占用不断上涨,进而带来页面卡顿、动画掉帧、操作延迟。JavaScript 引擎的垃圾回收器在内存压力过大时会更频繁地执行全停顿式 GC,用户感受到的卡顿,往往就是 GC 在“清扫房间”时整个程序暂停的瞬间。

排查内存泄漏,最常用的手段是浏览器的开发者工具。Memory(性能)面板里可以记录堆快照,连续拍几张快照并对比对象数量,就能看出哪些对象只增不减。Performance 面板则能录制一段操作过程,观察内存曲线的斜率——如果曲线一直上升、不回落,基本可以判断有泄漏。

打开 DevTools 的 Memory 面板,使用“堆快照”功能连续采样,再用“对比”视图过滤出新增不减的对象,就能一步步缩小范围。这个过程就像翻钱包:把里面的钥匙全部倒出来,看看哪些该扔、哪些该留,结论自然浮现。

卡顿程度∝当前堆内存−可回收内存内存预算\displaystyle \text{卡顿程度} \propto \frac{\text{当前堆内存} - \text{可回收内存}}{\text{内存预算}}
分子越大、分母越小,卡顿就越明显。通过对比快照,我们可以定位到那些“只增不减”的对象,再沿着持有它们的闭包引用,找到泄漏源头。
  1. 当前堆内存引擎分配给页面的所有对象所占用的总内存,包含正在使用和已无用的部分。
    Mheap=Mused+Mwasted\displaystyle M_{\text{heap}} = M_{\text{used}} + M_{\text{wasted}}
  2. 可回收内存被闭包等引用牢牢拉住、但实际上永远不会再被访问的那部分对象。
    Mwasted=Mheap−Mreachable\displaystyle M_{\text{wasted}} = M_{\text{heap}} - M_{\text{reachable}}
  3. 比例关系浪费越大,比例越接近甚至超过预算,GC 频率和停顿时间越严重,卡顿越明显。
    卡顿∝MwastedMbudget\displaystyle \text{卡顿} \propto \frac{M_{\text{wasted}}}{M_{\text{budget}}}

防范策略

整理钥匙串的正确姿势,是定期把不用的钥匙摘下来。代码层面的防范策略同样清晰:不再使用的闭包引用,及时置空;不需要的定时器,马上清除;元素销毁时解除事件绑定。

置空引用是最直接的策略:fn = null 或 callback = null,把闭包的最后一条引用路径断开,垃圾回收器就能在下一轮把它连带捕获的变量一起回收。另一种优雅的方案是用弱引用(WeakMap、WeakSet)来存储对象,弱引用不会阻止回收器清理对象,适合做缓存、存储与对象关联的辅助数据。

规范编码是更根本的防线:在 setInterval 或 setTimeout 的回调里尽量不引用大对象,或者在合适时机调用 clearInterval;使用事件委托而不是为每个子元素单独绑定大量监听器;在模块卸载时统一执行清理函数。记住一个原则——每次你“拿到钥匙”,都要想清楚什么时候“还回去”。

释放引用: ref→null⇒对象可回收\displaystyle \text{释放引用}:\ \text{ref} \to \text{null} \quad \Rightarrow \quad \text{对象可回收}
置空引用相当于把钥匙从背包里拿出来扔进回收站。GC 看到对象不再可达,就会在下一次清理时将它回收,内存曲线也随之回落。
  1. 切断引用把保存着闭包引用或对象引用的变量赋值为 null。
    ref:=null\displaystyle \text{ref} := \text{null}
  2. 失去可达性GC 在标记阶段发现对象不再从根节点出发可达,将其标记为可回收。
    对象∉ReachableSet\displaystyle \text{对象} \notin \text{ReachableSet}
  3. 回收内存GC 清扫阶段把不可达对象的内存释放,重新纳入可用内存池。
    Mfree←Mfree+Mobject\displaystyle M_{\text{free}} \gets M_{\text{free}} + M_{\text{object}}

作用域链

回家的路线总是清晰地分段:先走小区门,再上楼,最后掏钥匙开自家门。作用域链就是程序查找变量时的“回家路线”:从当前函数的最内层开始,逐级向外,直到全局作用域。这条链既决定了闭包能访问哪些变量,也决定了访问的顺序。

全局作用域是作用域链的终点,在任何函数里都能访问全局变量;局部作用域是函数自己创建的那一层,只属于函数内部。外层函数与内层函数的变量名如果相同,内层会优先使用自己的,这种“遮蔽”效应也源于作用域链的查找顺序。另一个相关特性是变量提升:var 声明的变量会被提升到函数顶部,但这只是声明提前,赋值仍留在原处。

理解了作用域链,闭包的一切行为都有了解释:为什么函数返回后还能读到外层变量?因为作用域链引用没断。为什么同名变量会优先取内层?因为查找顺序由内而外。这把“钥匙”能开多远、能开哪些门,全由作用域链决定。

查找方向: 内层函数→外层函数→全局作用域\displaystyle \text{查找方向}:\ \text{内层函数} \to \text{外层函数} \to \text{全局作用域}
作用域链像一列单向电梯,只能向外乘、不能向内坐。闭包能访问外层变量,正是因为这列电梯始终为它保留着。
  1. 内层函数作用域查找从当前函数自己的参数和局部变量开始。
    Scope0: 函数局部\displaystyle \text{Scope}_0:\ \text{函数局部}
  2. 外层函数作用域内层找不到,向上进入外层函数的环境记录继续查找。
    Scope1: 父级函数\displaystyle \text{Scope}_1:\ \text{父级函数}
  3. 全局作用域外层也找不到,最终抵达全局对象(window / globalThis)。
    Scopen: 全局对象\displaystyle \text{Scope}_n:\ \text{全局对象}
  4. 失败返回全局也找不到变量,引擎抛出 ReferenceError 异常。
    NotFound⇒Error\displaystyle \text{NotFound} \Rightarrow \text{Error}

AI 应用场景

前端组件内存治理

阶段:模型部署与前端集成

解决的问题:在 AI 前端页面(如大模型对话界面、实时推荐面板)中,组件频繁挂载和卸载。传统做法下,回调函数和定时器容易残留,造成内存泄漏,页面越用越卡。AI 通过闭包封装组件的“创建—销毁”生命周期,在组件卸载时统一执行清理函数,从而精确回收资源。

具体案例:某智能客服页面每秒轮询一次大模型接口,将用户对话状态保存在闭包中。页面组件卸载后,定时器未清理,导致内存持续上升。开发者使用闭包封装轮询器,并在组件卸载钩子中调用释放函数,内存曲线恢复平稳。

数据可视化大屏性能优化

阶段:数据采集与实时渲染

解决的问题:AI 大屏需要每秒接收大量指标数据并渲染图表,每次刷新都创建新的闭包来捕获数据快照。如果旧闭包不释放,数据堆积会让渲染逐渐卡顿。利用闭包封装数据过滤器,并在每次更新时置空旧引用,能保证内存可控。

具体案例:某调度中心大屏使用 WebSocket 推送设备状态,前端每秒用闭包记录最新一条数据并渲染。上线后发现 2 小时后页面明显卡顿。通过堆快照定位到闭包持有的历史数据数组,改为每次推送后置空旧数组,运行 24 小时内存稳定。

Python 演示

python62 行
# Python 中模拟 JS 闭包,并演示内存释放机制import gcimport weakref  def make_counter(start=0):    """创建闭包:内部函数可访问外层函数的 count 变量"""    count = start     def step():        nonlocal count        count += 1        return count     return step  # 闭包基本应用:计数器持久工作c = make_counter(5)print("第一次:", c())print("第二次:", c())print("第三次:", c())  # 闭包持有外层大对象,导致 GC 无法回收def create_closure():    big_data = [1] * 1000000     def get_size():        return len(big_data)     return get_size, weakref.ref(big_data)  fn, data_ref = create_closure()print("闭包持有 big_data:", data_ref() is not None) # 释放闭包的最后引用后,大对象被回收del fngc.collect()print("删除闭包后 big_data 已回收:", data_ref() is None)  # 弱引用缓存:不阻碍垃圾回收cache_pool = weakref.WeakValueDictionary()  class TempObject:    pass  def cached_get(key):    obj = TempObject()    cache_pool[key] = obj    return obj  obj = cached_get("demo")print("缓存存在:", "demo" in cache_pool)del objgc.collect()print("删除强引用后缓存自动移除:", "demo" not in cache_pool)
目的:用 Python 直观演示闭包“函数 + 词法环境”的绑定关系,并用 weakref 验证释放闭包后变量被回收、弱引用缓存自动清理的过程。
  1. nonlocal countPython 声明闭包内的变量修改需要 nonlocal,对应 JavaScript 闭包捕获外层变量的机制。
  2. count += 1每次调用修改闭包环境中的 count,让计数器持久自增。
  3. weakref.ref(big_data)创建一个不阻止对象回收的“弱引用”,用来观察 big_data 是否还存活。
  4. del fn删除闭包的最后外部引用,等价于把 JS 闭包变量置为 null。
  5. gc.collect()手动触发垃圾回收,让引擎立刻清扫不可达对象。
  6. WeakValueDictionaryPython 版的弱引用字典,对应 JS 里的 WeakMap 思路。
  7. "demo" not in cache_pool强引用删除后,弱引用字典自动移除了条目,证明没有泄漏。

常见误区

误区我听说闭包能保存外部变量,所以用闭包就会让变量永远不释放,闭包就等于内存泄漏。

正解闭包本身是语言特性,不是 bug。只有当闭包被全局变量、定时器等长期持有,且内部引用的对象已不再需要时,才构成泄漏。用完之后及时置空引用或解除绑定,闭包就是安全的。

为何常见:初学者在排查泄漏时频繁看到闭包的身影,容易把“受害者”当成“凶手”,忽略了真正的持有者——全局引用或事件绑定。

误区函数调用结束,里面的变量就销毁了,内层函数返回后读取外部变量肯定会报错。

正解对于普通局部变量,调用结束确实销毁;但被内层函数捕获的变量会转入堆内存,只要闭包还存活,这些变量就继续存在。这就是闭包能够“穿越时间”的原理。

为何常见:习惯了 C 或 C++ 的栈帧模型,容易忽略 JavaScript 的垃圾回收和词法环境机制,把“调用结束销毁”当成铁律。

误区`let` 和 `const` 有块级作用域,肯定比 `var` 更安全,不会造成闭包泄漏。

正解let 解决的是变量提升和块级作用域问题,但闭包捕获的是“变量引用”而非声明方式。let 声明的变量同样会被闭包捕获并保持存活,该泄漏还是会泄漏。

为何常见:大部分人记住了 let 的好处,却把它当成万能解药,忽略了内存泄漏的核心是引用生命周期管理。

误区JS 引擎有自动垃圾回收,就算我忘了清理引用,GC 也会在某个时刻把所有无用对象回收掉。

正解GC 只回收“不可达”对象。只要闭包引用链从全局根节点出发依然可达,GC 就会认为对象还在使用,不会回收。自动回收不等于自动释放,可达性是回收的前提。

为何常见:把“自动”理解为“全能”,忽略了 GC 的工作前提,直到内存曲线飙升才意识到问题的严重性。

面试题

Q1. 闭包最核心的特征是什么?

单选题 · easy

  • A.函数内部可以定义另一个函数
  • B.内层函数访问外层函数的变量← 正确答案
  • C.函数能返回一个数字或字符串
  • D.函数在全局作用域中执行

答案:B。函数嵌套只是前提,真正的核心在于内层函数通过词法环境引用外层变量,从而形成闭包。选项 C、D 与闭包无关。

Q2. 外层函数返回内部函数后,调用内部函数访问外层局部变量会怎样?

单选题 · easy

  • A.抛出 ReferenceError 异常
  • B.得到 undefined 空值
  • C.返回全局同名变量的值
  • D.仍能读到外层局部变量← 正确答案

答案:D。内部函数携带词法环境的引用,外层函数虽然返回,但变量没有销毁,因此内部函数仍然可以读到外层局部变量,这就是闭包的核心行为。

Q3. 闭包导致内存泄漏的根本原因是什么?

单选题 · mid

  • A.变量声明使用了 var 关键字
  • B.JavaScript 没有垃圾回收机制
  • C.引用持续可达导致对象无法回收← 正确答案
  • D.函数执行速度过慢阻塞内存

答案:C。GC 只回收不可达对象,闭包把变量对象挂在可达引用链上,对象就不会被回收。与 var、执行速度没有直接关系。

Q4. 以下哪种方式通常不会导致闭包泄漏?

单选题 · mid

  • A.定时器回调引用大型列表数据
  • B.全局变量保存不再使用的闭包
  • C.DOM 卸载后监听器未解除
  • D.函数内使用局部基础类型常量← 正确答案

答案:D。局部基础类型常量不会被闭包长期持有,也不会成为泄漏源。前三项都会让闭包或监听器持续保持可达引用,造成内存占用上升。

Q5. 以下哪些场景容易触发闭包内存泄漏?

多选题 · mid

  • A.setInterval 的回调持有大对象← 正确答案
  • B.事件监听器注册后从未移除← 正确答案
  • C.卸载组件时忘记清理回调← 正确答案
  • D.普通函数内部声明临时变量

答案:ABC。定时器、事件监听和组件回调都是闭包的常见栖息地。如果没有及时清理,它们会让捕获的对象长期可达,形成泄漏。D 的临时变量随函数结束自动释放。

Q6. 将不再使用的闭包引用设置为 null 的主要作用是什么?

单选题 · hard

  • A.立即释放闭包所占用的全部内存
  • B.断开可达引用让 GC 可以回收← 正确答案
  • C.让闭包函数立刻失效并报错
  • D.强制 JavaScript 引擎停止运行

答案:B。置空不会同步释放内存,它只是把对象从可达引用链上摘下来。GC 在下一轮标记-清扫时发现对象不可达,才会回收内存。

Q7. 关于闭包和作用域链的说法,正确的有哪些?

多选题 · hard

  • A.闭包可以用于模拟私有变量← 正确答案
  • B.闭包会延长捕获变量的生命周期← 正确答案
  • C.闭包只能读取调用时刻的变量值
  • D.作用域链查找由内层向外层进行← 正确答案

答案:ABD。闭包通过环境保存引用,能延长变量生命周期,也能模拟私有变量。它读取的是词法创建时刻的绑定,并非调用时刻的快照。作用域链确实由内向外查找。

Q8. 下列哪种数据结构适合做闭包相关的弱引用缓存?

单选题 · mid

  • A.全局数组 Array
  • B.强引用对象 Object
  • C.WeakMap 弱引用集合← 正确答案
  • D.字符串拼接结果

答案:C。WeakMap 的键是弱引用,不会阻止垃圾回收器清理对象,非常适合存放与对象关联的缓存数据,避免传统缓存导致的内存泄漏。

Q9. 作用域链查找变量的正确顺序是什么?

单选题 · easy

  • A.全局作用域 → 外层函数 → 局部
  • B.外层函数 → 全局 → 局部
  • C.局部 → 外层函数 → 全局← 正确答案
  • D.全局 → 局部 → 外层函数

答案:C。变量查找总是从当前函数自己的作用域开始,找不到再逐级向外,最后才到全局。顺序颠倒会导致逻辑错误。

Q10. 关于闭包与内存释放,下列说法正确的有哪些?

多选题 · hard

  • A.置空引用后 GC 会在合适的时机回收对象← 正确答案
  • B.使用 WeakMap 可以避免缓存造成的泄漏← 正确答案
  • C.闭包一定会在内存中占据大量空间
  • D.事件监听器可以在移除时解除闭包引用← 正确答案

答案:ABD。置空引用让对象失去可达性,GC 因此可以回收;WeakMap 的弱引用特性是缓存防泄漏的常用方案;移除事件监听及时切断闭包链。C 的正确说法应该是“闭包不一定占大量空间,只有持有了大对象才明显”。