不知不觉间,成为一名前端工程师也有几年时间了。目前好像遇见了瓶颈期,一些基础知识经常忘却,底层原理和知识也没有学习得很透彻。
因此,我打算重学一些前端的基础知识,并在掘金上记录学习📝笔记。
如果你也遇到同样的问题,欢迎加入我一起温故知新吧。
📊 闭包知识图谱
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
🧐 发生了什么?
outer()
执行后,count = 0
初始化。inner()
被返回并赋值给counter
。- 关键:外部函数
outer()
执行结束,但inner()
仍然可以访问count
变量。 - 每次调用
counter()
,count
仍然保留在内存中,使得值可以累加。
2. 作用/重要性
✅ 保持数据私有性 :外部作用域无法直接修改内部变量,但可以通过闭包访问或修改。
✅ 数据持久化 :函数执行完毕后,变量不会立即销毁。
✅ 模拟对象的私有方法 (类似于 class
的封装)
3. 影响/注意事项
存在常见的问题是,可能会导致内存泄漏。
-
如果闭包函数被长时间保存(例如绑定在全局变量上或绑定 DOM 事件),而没有手动释放对外部变量的引用,就可能导致它们无法被垃圾回收。
-
例如:
jsfunction funA() { var largeArray = new Array(1000000).fill("data"); // 产生大量占用内存的数据 return function() { console.log(largeArray.length); }; } var b = funA(); // 此时 largeArray 依然存在,可能会消耗内存
4. 一些相关的高阶问题
1. 当闭包被绑定到 DOM 事件处理函数上,可能会导致无法正确回收内存,如何避免?
-
- 组件卸载时,应使用
removeEventListener
解绑事件 ✅(React、Vue 等框架)
- 组件卸载时,应使用
-
- 在手动绑定事件的地方,解绑后应清空变量引用 ✅(
handler = null;
)
- 在手动绑定事件的地方,解绑后应清空变量引用 ✅(
-
- 使用
WeakMap
存储数据,防止对象被闭包长期引用 ✅
- 使用
-
- 尽量避免在长生命周期的闭包中持有大量数据 ✅
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
,不会更新。 -
太频繁的事件解绑 & 重新绑定可能影响性能 ,可以使用
useCallback
或useRef
来优化:jsximport { 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> </> ); }
✅
useRef
让handleClick
访问到最新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() (见前文代码示例) |
🚨 闭包内存泄漏自检清单
- 是否在闭包中引用了 DOM 元素? → 使用
WeakMap
存储关联数据 - 是否未解绑事件监听? → 组件卸载时调用
removeEventListener
- 是否缓存了不再使用的大数据? → 手动置空变量:
bigData = null;
🔚 终极结语
闭包如同 JavaScript 的「双刃剑」:
- 用得好:它是实现模块化、封装状态的利器,让代码如瑞士军刀般灵活;
- 用不好:它会让内存像破洞的水桶一样悄悄流失,最终拖垮应用性能。
记住:能力越大,责任越大 。下次写出 function outer() { ... }
时,不妨灵魂三问:
- 这个闭包真的需要存在吗?
- 它引用的数据是否可能成为内存钉子户?
- 有没有更优雅的替代方案(如 Class、模块化)?