一次讲透 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
  • 注意事项:大对象闭包引用可能导致内存泄漏
相关推荐
吠品1 小时前
STM32最小系统板引脚梳理与配置实操
前端·javascript·vue.js
程序员清风1 小时前
聊聊我的AI学习方法与思考!
java·后端·面试
Hilaku1 小时前
Sass 和 Less 在 2026 年彻底多余了吗?
前端·javascript·程序员
右耳朵猫AI1 小时前
Node.js周刊2026W38 | 进程中断缺陷修复、Copilot 迁至 Rust、Node 新增 VFS
javascript·后端·node.js
Shao2391 小时前
基于 Rokid AIUI:在眼镜上穿越五千年
javascript
咩咩啃树皮1 小时前
ES6 Set 核心特点 + 最简数组去重
前端·javascript·html
OpsEye1 小时前
开发者小技巧:一套API Key调用海内外多款代码大模型
javascript·ai编程
流浪0011 小时前
Linux系统篇36——线程(一) 线程的概念、本质和Linux的实现方式
linux·面试·操作系统·线程