一次讲透 JS 闭包 —— 概念、原理、应用和内存泄漏

先看一个现象:

js 复制代码
function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
counter();  // 1
counter();  // 2
counter();  // 3

makeCounter 执行完了,变量 count 应该销毁才对,为啥 counter() 每次调用还能记得?

答:闭包。

一、闭包是什么

闭包 = 函数 + 它能访问的外部作用域。

说人话:一个函数被"带出"外层作用域时(通过 return 或作为参数传出),它保持了对定义时作用域的引用,即使外层函数已经执行完毕。

上面的例子:

  • makeCounter 是外层函数,内部有变量 count
  • 返回的匿名函数引用了 count
  • 匿名函数被赋值给 counter 存活下来
  • 它带着对 count 的引用一起活了下来

这就是闭包。

二、为什么 count 没被销毁

正常情况下,函数执行完 → 局部变量被 GC 回收。

但是:

  • 只要有其他地方引用这个变量,GC 就不会回收它
  • 上面的匿名函数引用了 count
  • 匿名函数被赋值给 counter 存活下来
  • 所以 count 一直被引用 → GC 不回收

一句话:变量的生命周期,由"还有没有人引用它"决定,不由"函数执行没执行完"决定。

三、闭包常见的 3 个用途

用途 1:数据私有化(模拟私有变量)

js 复制代码
function createUser(name) {
  let _password = '123456';  // 外部访问不到
  
  return {
    getName: () => name,
    checkPassword: (pwd) => pwd === _password,
  };
}

const user = createUser('张三');
user.getName();               // "张三"
user.checkPassword('123456'); // true
user._password;                // undefined ------ 外部拿不到

_password 被闭包保护,外部无法访问,但内部方法能用。

这是 ES6 前实现"私有变量"的经典模式。

用途 2:防抖 / 节流

js 复制代码
function debounce(fn, delay) {
  let timer = null;  // 被闭包保存
  
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const handler = debounce(() => console.log('搜索'), 300);
input.addEventListener('input', handler);

timer 变量被内层函数闭包捕获,每次输入都能拿到上一次的 timer 清除掉。

用途 3:React Hooks 底层就是闭包

js 复制代码
function useState(initial) {
  let state = initial;  // 被闭包保存
  
  const setState = (newState) => {
    state = newState;
  };
  
  return [state, setState];
}

React 的 useState 本质就是闭包 ------ state 在函数式组件里能"保持"住,靠的就是闭包。

四、经典陷阱:for + var + setTimeout

面试常考这道题:

js 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出:3 3 3

为啥不是 0 1 2?

因为 var 是函数作用域 ,i 只有一份。3 个 setTimeout 的回调闭包捕获的是同一个 i ,循环结束后 i 已经变成 3。

解决方案 1 :用 let(块级作用域)

js 复制代码
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出:0 1 2

let 在每次循环创建新的作用域,每个 setTimeout 闭包捕获自己那一份 i。

解决方案 2:立即执行函数(IIFE)

js 复制代码
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}
// 输出:0 1 2

用 IIFE 把 i 传进去,变成参数 j,每次循环都是一个新的闭包。

五、闭包会导致内存泄漏吗?

闭包本身不会。 泄漏的根源是**"忘了清理引用"**,不是闭包本身。

大部分闭包场景是安全的:

  • 防抖节流 → 事件移除时,闭包自动回收
  • React useState → 组件卸载时,闭包自动回收
  • 私有变量 → 对象被回收时,闭包自动回收

真正需要警惕的是:大对象 + 长期未清理的引用。

举例:

js 复制代码
function attach() {
  const bigData = new Array(1000000).fill('data');
  document.getElementById('btn').addEventListener('click', () => {
    console.log(bigData.length);
  });
}
attach();
// bigData 永远回收不了,因为 click 事件一直引用它

养成习惯:

  • 每次 addEventListener → 想着 removeEventListener
  • 每次 setInterval → 想着 clearInterval
  • 图表类库 → 组件卸载时 dispose

结论 :别怕用闭包,记得清理引用就行。

六、总结

闭包 = 函数 + 它能访问的外部作用域。

3 个关键点:

  • 产生原因:内部函数引用了外部作用域的变量,外部函数执行完变量也不被回收
  • 常见应用:私有变量、防抖节流、React Hooks
  • 注意事项:大对象闭包引用可能导致内存泄漏
相关推荐
Zelman4 分钟前
测试层级与测试类型
后端·面试·测试
考研保研资料分享36 分钟前
自动化控制保研经验合辑:上交夏令营、北工大预推免与跨专业面试复盘(2025)
运维·面试·自动化
杨杰3982 小时前
我给嵌入式设备写了个 6MB 的 JavaScript 运行时(xuan)
javascript
怕浪猫3 小时前
Agent 工程化面试:从开发到部署的 5 个关键问题
面试·架构·github
只睡四小时4 小时前
AI 生成 PPTX:11 页课件编译出 429 个形状
javascript·python·pptx·ai生成ppt·ooxml
用户6919026813394 小时前
LangChain 与 LangGraph:从「链」到「图」,一次讲清它们是什么、什么关系
面试·架构·设计
shmily麻瓜小菜鸡4 小时前
Axios 中 params 与 data 的区别与原理
javascript·typescript
isha4 小时前
Univer拆解——从类与方法的视角分析
前端·javascript
CoderYanger5 小时前
A.每日一题:856. 括号的分数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
变与不变8065 小时前
GET请求知识详解
前端·javascript