← 返回题目列表

什么是闭包?闭包有哪些应用和问题?

高频 中等 第 7 / 29 题 更新于 2026/07/27
JavaScript闭包作用域

简化版

闭包是函数和它能访问的外层词法环境的组合。即使外层函数执行完,内部函数仍然可以访问外层变量。它常用于封装私有变量、函数工厂、回调保存状态,但滥用可能导致内存无法及时释放。

详细版

示例:

function createCounter() {
  let count = 0;
  return function () {
    return ++count;
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2

createCounter 执行完后,count 没有被销毁,因为返回的内部函数仍然引用它。

应用场景:

  • 私有状态封装。
  • 防抖、节流保存定时器。
  • 函数柯里化和函数工厂。
  • 事件回调保留上下文信息。

风险是闭包引用了大对象或 DOM 后,如果回调长期存在,相关内存也会被保留。

完整版教学

一、闭包来自词法作用域

JavaScript 使用词法作用域,函数能访问哪些变量,在函数定义时就确定了,而不是调用时才确定。

内部函数定义在外部函数里面,所以它天然能访问外部函数的局部变量。当内部函数被返回或传到外部时,这种访问能力仍然存在。变量解析会沿函数定义位置对应的环境链向外查找,因此把函数换到另一个调用位置并不会改成动态作用域。

二、闭包不是特殊语法

闭包不是某个关键字,而是一种运行时现象:

function outer() {
  const name = 'AIGC';
  function inner() {
    console.log(name);
  }
  return inner;
}

inner 和它引用的 name 一起形成闭包。

三、循环闭包经典问题

使用 var 时,循环变量是函数作用域,多个回调共享同一个变量:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3

使用 let 后,每次循环都有新的块级绑定:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2

四、闭包和内存泄漏

闭包本身不是内存泄漏。问题出在闭包长期引用不再需要的数据,比如已经移除的 DOM 节点、很大的数组、定时器回调等。

处理方式是及时解绑事件、清除定时器、让不再需要的引用断开。现代垃圾回收器能够处理不可达的循环引用,所以真正要排查的是从全局对象、监听器等根到闭包环境的可达路径,而不是引用是否成环。

五、面试追问与工程落地

闭包常见追问是“闭包一定会造成内存泄漏吗?”答案是否定的。闭包只是让函数保留外层变量引用,只有当这些引用长期不释放,并且保留了不再需要的大对象或 DOM 时,才可能造成泄漏。

另一个高频追问是模块化中的私有变量。早期 JS 没有 class private fields 时,常用闭包封装私有状态。现在也可以用闭包写函数工厂、组合式逻辑、React hooks、Vue composables。它不是过时技巧,而是 JS 作用域模型的基础能力。

工程里使用闭包要注意生命周期。比如防抖函数保存 timer、请求函数保存 token、事件回调保存组件状态,都要确认这些闭包什么时候被释放。组件卸载时如果监听器没解绑,闭包里引用的状态也会继续活着。

六、用环境记录推演闭包的生命周期

执行 createCounter() 时,引擎会为这次调用建立词法环境,count 绑定属于该环境。返回函数内部仍保存着解析 count 所需的外层环境引用,所以调用栈弹出不等于环境立即可回收;只有返回函数以及其他可达引用都消失后,相关环境才可能被垃圾回收。

全局环境
└─ counter ──> 返回函数
               └─ [[Environment]] ──> { count: 2 }

若分别执行两次 createCounter(),会得到两个函数和两套独立绑定。调用顺序为 a()a()b() 时结果是 1、2、1,因为闭包捕获的是各次调用创建的绑定环境,而不是共享同一个名为 count 的全局变量。

场景捕获的绑定生命周期风险
函数工厂每次外层调用的新环境实例过多会保留多份状态
for (let ...) 回调每轮新的迭代绑定回调数量仍决定保留时长
防抖函数timer 与最近参数未取消的回调会延长引用生命周期
全局事件监听器监听器可达的外层数据不解绑可能长期保留组件对象

闭包保留的是词法环境中的绑定访问能力,不是简单地把变量当前值复制一份;判断输出时要看多个函数是否引用同一个绑定。

七、常见误区与追问

  • 误区:只要函数嵌套就一定产生需要长期存活的闭包。 嵌套函数具备访问外层绑定的词法条件,但只有函数逃逸或被后续调用时,长期保留现象才对程序可观察。
  • 误区:外层函数返回后,它的全部局部变量都会立刻销毁。 垃圾回收看可达性;仍被内部函数访问的环境不能仅因调用栈退出就回收。
  • 误区:闭包捕获的是变量创建那一刻的值副本。 闭包通常访问同一个绑定,所以外层代码之后修改该绑定,闭包读取到的是修改后的值。
  • 追问:为什么 var 循环回调常打印同一个最终值? var 使用同一个函数或全局作用域绑定,所有回调在循环结束后读取的都是同一变量。
  • 追问:为什么 let 能得到每轮不同的值? for 的词法声明会为每次迭代创建新的绑定环境,使每个回调关联各自那一轮的 i
  • 追问:怎样判断闭包是否造成内存泄漏? 看持有闭包的根引用是否超出业务生命周期,以及其环境是否保留了本应释放的大对象,而不是看到闭包就判泄漏。
  • 追问:闭包与对象属性保存状态有什么区别? 两者都能保存状态;闭包通过词法作用域隐藏绑定,对象属性则更便于枚举、序列化和显式管理。

八、加强记忆

闭包可以记成“函数背着出生地的变量包”。它让函数离开定义位置后还记得外层变量。好处是保存状态,坏处是状态可能被保存太久。