【温故知新 · JavaScript 】闭包📒

不知不觉间,成为一名前端工程师也有几年时间了。目前好像遇见了瓶颈期,一些基础知识经常忘却,底层原理和知识也没有学习得很透彻。

因此,我打算重学一些前端的基础知识,并在掘金上记录学习📝笔记。

如果你也遇到同样的问题,欢迎加入我一起温故知新吧。

📊 闭包知识图谱

js 复制代码
                    ┌──────────────┐
                    │   闭包的本质   │
                    │ 函数 + 词法环境 │
                    └──────┬───────┘
                           │
           ┌────────────────┴─────────────────┐
           ▼                                  ▼
┌─────────────────────┐            ┌─────────────────────┐
│      核心价值        │            │      潜在风险        │
│  ✔ 数据封装与持久化    │            │  ❌ 内存泄漏         │
│  ✔ 模拟私有变量       │            │  ❌ 性能损耗         │
└──────────┬──────────┘            └──────────┬──────────┘
           │                                  │
           ▼                                  ▼
┌─────────────────────┐            ┌─────────────────────┐
│     应用场景         │            │     防御策略         │
│  🔹 计数器/缓存      │            │  🔸 WeakMap/Set     │
│  🔹 高阶函数         │            │  🔸 及时解绑事件     │
│  🔹 异步状态管理     │            │  🔸 React ref优化   │
└─────────────────────┘            └─────────────────────┘

1. 定义/本质( Closures in mdn

闭包是指一个函数可以访问其外部作用域(即使它在外部作用域之外执行)。

在 JavaScript 中,每当函数创建时,它都会携带一个对**定义它的词法环境(Lexical Environment)**的引用。 即使这个函数在其作用域之外执行,它仍然可以沿着作用域链访问最初定义时的环境变量。


简单示例:

js 复制代码
function outer() {
  let count = 0; // 外部变量

  return function inner() { 
    count++; // 访问外部变量
    console.log(count);
  };
}

const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2

🧐 发生了什么?

  1. outer() 执行后,count = 0 初始化。
  2. inner() 被返回并赋值给 counter
  3. 关键:外部函数 outer() 执行结束,但 inner() 仍然可以访问 count 变量
  4. 每次调用 counter()count 仍然保留在内存中,使得值可以累加。

2. 作用/重要性

保持数据私有性 :外部作用域无法直接修改内部变量,但可以通过闭包访问或修改。

数据持久化 :函数执行完毕后,变量不会立即销毁。

模拟对象的私有方法 (类似于 class 的封装)

3. 影响/注意事项

存在常见的问题是,可能会导致内存泄漏。

  • 如果闭包函数被长时间保存(例如绑定在全局变量上或绑定 DOM 事件),而没有手动释放对外部变量的引用,就可能导致它们无法被垃圾回收。

  • 例如:

    js 复制代码
    function funA() {
      var largeArray = new Array(1000000).fill("data"); // 产生大量占用内存的数据
      return function() {
        console.log(largeArray.length);
      };
    }
    var b = funA();  // 此时 largeArray 依然存在,可能会消耗内存

4. 一些相关的高阶问题

1. 当闭包被绑定到 DOM 事件处理函数上,可能会导致无法正确回收内存,如何避免?

    1. 组件卸载时,应使用 removeEventListener 解绑事件 ✅(React、Vue 等框架)
    1. 在手动绑定事件的地方,解绑后应清空变量引用 ✅(handler = null;
    1. 使用 WeakMap 存储数据,防止对象被闭包长期引用
    1. 尽量避免在长生命周期的闭包中持有大量数据

2. 为什么使用weekMap可以避免内存泄漏?

👉 WeakMap 是弱引用,不会阻止 GC 回收

js 复制代码
const cache = new WeakMap();
function saveData(obj, value) {
  cache.set(obj, value);
}

let user = { id: 1 };
saveData(user, "Alice");

// 当 user 变成 null,没有其他引用时,它会被垃圾回收
user = null;  // ✅ WeakMap 不会阻止 GC,存储的值会自动释放

WeakMap 只能是对象 ,它是弱引用不会影响对象的生命周期,如果对象的其他地方没有被引用,**它会自动被 GC

👉 什么是内存泄漏? 在 JavaScript 中,如果我们创建了一个对象 ,但它仍然被某些变量引用 ,即使这个对象已经不再使用,*GC(垃圾回收)也不会释放它

比如:

js 复制代码
const cache = new Map();
function saveData(key, value) {
  cache.set(key, value);
}

saveData('username', { name: 'Alice' });
// 即使 'username' 没用了,这个对象还在 cache 里,占用内存

由于 Map 强引用value,即使外部不再使用 { name: 'Alice' },这个对象依然占用内存!


3. 如果有多个事件监听,如何统一管理它们的解绑?

在管理多个事件时,可以使用 Map(或 WeakMap,视情况而定)来存储所有的事件处理函数,后续遍历解绑。例如:

js 复制代码
const eventRegistry = new Map();

function attachEvent(target, eventType, handler) {
  target.addEventListener(eventType, handler);

  // 存储事件
  if (!eventRegistry.has(target)) {
    eventRegistry.set(target, new Map());
  }
  eventRegistry.get(target).set(eventType, handler);
}

function detachAllEvents() {
  eventRegistry.forEach((events, target) => {
    events.forEach((handler, eventType) => {
      target.removeEventListener(eventType, handler);
    });
  });
  eventRegistry.clear(); // 清空 Map,防止数据泄漏
}

// 示例用法
const btn = document.getElementById("myButton");

attachEvent(btn, "click", () => console.log("Clicked!"));
attachEvent(btn, "mouseover", () => console.log("Hovered!"));

// 🌟 需要解绑时调用:
detachAllEvents();

📌 优点:

  • 统一管理:不需要手动记住所有绑定的事件,可以随时遍历解绑。
  • 避免遗忘解绑:在组件卸载时,可以一次性清理所有事件,防止内存泄漏。
  • 可扩展:可以用于复杂的组件或多事件管理。

4. React 中 useEffect 依赖数组 [deps] 里的值会不会影响事件绑定的行为?

React useEffect 中,依赖数组 [deps] 确实会影响事件的绑定行为。例如:

jsx 复制代码
import { useEffect, useState } from "react";

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    function handleClick() {
      console.log(`Button clicked! Current count: ${count}`);
    }

    document.getElementById("myButton").addEventListener("click", handleClick);

    return () => {
      document.getElementById("myButton").removeEventListener("click", handleClick);
    };
  }, [count]); // ✅ count 变化时,事件会重新绑定

  return (
    <>
      <button id="myButton">Click Me</button>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </>
  );
}

🧐 发生了什么?

  • 每次 count 变化,会触发 useEffect 重新执行
  • 旧的 handleClick 被移除,新的 handleClick 被重新绑定,使 console.log 能够访问最新的 count 值。

📌 注意事项:

  • 如果 deps 为空 [],事件处理函数只能访问挂载时的 count ,不会更新

  • 太频繁的事件解绑 & 重新绑定可能影响性能 ,可以使用 useCallbackuseRef 来优化:

    jsx 复制代码
    import { useEffect, useRef } from "react";
    
    function MyComponent() {
      const countRef = useRef(0); // ✅ 使用 ref 记录最新的 count
    
      useEffect(() => {
        function handleClick() {
          console.log(`Count: ${countRef.current}`);
        }
    
        document.getElementById("myButton").addEventListener("click", handleClick);
        
        return () => {
          document.getElementById("myButton").removeEventListener("click", handleClick);
        };
      }, []); // ✅ 只在组件挂载时绑定事件,不会每次 render 重新绑定
    
      return (
        <>
          <button id="myButton">Click Me</button>
          <button onClick={() => countRef.current++}>Increase Count</button>
        </>
      );
    }

    useRefhandleClick 访问到最新 countRef.current,避免频繁解绑 & 绑定。


5. 如果有成千上万的闭包对象,垃圾回收器(GC)是如何处理的?

JavaScript 的垃圾回收(GC)机制是**基于可达性分析(Reachability Analysis)**的:

  • 只要对象仍然被引用,GC 就不会回收。
  • 如果闭包的变量不再被任何活动代码引用,GC 就会回收它。

6. 闭包如何在异步编程 (Promise / async-await) 中发挥作用?

异步代码需要在执行上下文(Execution Context)销毁后,仍然访问特定变量。例如:

js 复制代码
function fetchData() {
    let id = 1001;  // 这个变量在函数执行完后仍然需要被异步代码访问
    setTimeout(() => {  
        console.log(`Fetching data for ID: ${id}`);
    }, 2000);
}

fetchData();  // `fetchData` 结束了,但 2s 后仍然打印 "Fetching data for ID: 1001"

解释:

  • 这里 setTimeout 里的回调创建了闭包 ,保存了 id,即使 fetchData 已经执行完成,但 id 仍然保留,直到回调执行完毕并被 GC

同样,闭包使 async/await 能够保持状态,比如:

js 复制代码
function createAsyncCounter() {
    let count = 0;
    return async function () {
        await new Promise(resolve => setTimeout(resolve, 1000));
        count++;
        console.log(count);
    }
}

const counter = createAsyncCounter();
counter();  // 1秒后打印 `1`
counter();  // 1秒后再打印 `2`

解释:

  • 内部 await 让函数暂停执行,但 count 变量仍活着,因为闭包保持了其作用域
  • 在多次调用时,闭包让 createAsyncCounter 初始化的 count 变量不会消失,确保 counter() 每次都能访问它。

🛠️ 闭包实战工具箱

场景 工具 代码示例片段
避免内存泄漏 WeakMap 弱引用 const cache = new WeakMap(); cache.set(target, data);
React 状态管理 useRef + useEffect const countRef = useRef(0); useEffect(() => { countRef.current = 1; }, [])
事件解绑 事件管理器 + 统一卸载 detachAllEvents() (见前文代码示例)

🚨 闭包内存泄漏自检清单

  1. 是否在闭包中引用了 DOM 元素? → 使用 WeakMap 存储关联数据
  2. 是否未解绑事件监听? → 组件卸载时调用 removeEventListener
  3. 是否缓存了不再使用的大数据? → 手动置空变量:bigData = null;

🔚 终极结语

闭包如同 JavaScript 的「双刃剑」:

  • 用得好:它是实现模块化、封装状态的利器,让代码如瑞士军刀般灵活;
  • 用不好:它会让内存像破洞的水桶一样悄悄流失,最终拖垮应用性能。

记住:能力越大,责任越大 。下次写出 function outer() { ... } 时,不妨灵魂三问:

  1. 这个闭包真的需要存在吗?
  2. 它引用的数据是否可能成为内存钉子户?
  3. 有没有更优雅的替代方案(如 Class、模块化)?
相关推荐
小满zs13 分钟前
React-router v7 第一章(安装)
前端·react.js
程序员小续19 分钟前
前端低代码架构解析:拖拽 UI + 代码扩展是怎么实现的?
前端·javascript·面试
wangpq27 分钟前
微信小程序地图callout气泡图标在ios显示,在安卓机不显示
前端·vue.js
curdcv_po30 分钟前
Vue3 组件通信方式全解析
前端
Auroral15635 分钟前
基于RabbitMQ的异步通知系统设计与实现
前端·后端
栗筝i35 分钟前
Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲
前端·网络·spring
打野赵怀真38 分钟前
H5如何禁止动画闪屏?
前端·javascript
zhangxingchao38 分钟前
关于浮点数的思考
前端
Riesenzahn38 分钟前
你喜欢Sass还是Less?为什么?
前端·javascript