闭包是什么?优缺点、怎么防内存泄漏?
📖 导读:闭包是 JavaScript 中最重要也最容易混淆的概念之一。它既是面试必考题,也是日常开发中的利器。本文将从基础到实战,带你彻底理解闭包,并掌握如何避免其带来的内存泄漏问题。
🎯 什么是闭包?
官方定义
闭包(Closure) 是指有权访问另一个函数作用域中变量的函数。
简单来说:当一个函数能够记住并访问它的词法作用域时,就产生了闭包。
通俗理解
想象一个场景:
- 你有一个盒子(外层函数)
- 盒子里有一些玩具(变量)
- 你把一个小机器人(内层函数)放进盒子
- 即使把盒子盖上拿走,小机器人仍然可以玩盒子里的玩具
这个小机器人就是闭包!
💡 基础示例
最简单的闭包
javascript
function outer() {
const name = '张三'; // 外层函数的变量
function inner() {
console.log(name); // 内层函数访问外层变量
}
return inner; // 返回内层函数
}
const closure = outer();
closure(); // "张三" - 即使 outer 已经执行完毕,仍能访问 name
关键点
- ✅
inner函数在outer外部被调用 - ✅
inner仍然可以访问outer中的name变量 - ✅ 这就是闭包!
🔍 闭包的原理
作用域链
JavaScript 采用词法作用域(静态作用域),函数的作用域在定义时就确定了。
javascript
function outer() {
const a = 10;
function middle() {
const b = 20;
function inner() {
const c = 30;
console.log(a + b + c); // 60
}
return inner;
}
return middle;
}
const fn = outer()();
fn(); // 60
作用域链查找过程:
inner 的作用域链:
1. inner 自身作用域 (c)
2. middle 的作用域 (b)
3. outer 的作用域 (a)
4. 全局作用域
内存中的表现
javascript
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
内存结构:
createCounter 执行完毕后:
- 正常情况下,count 应该被销毁
- 但因为闭包的存在,count 仍然保存在内存中
- counter 函数持有对 count 的引用
✨ 闭包的优点
1. 数据封装与私有变量
闭包可以创建私有变量,外部无法直接访问。
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
console.log(`存入 ${amount},余额:${balance}`);
}
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
console.log(`取出 ${amount},余额:${balance}`);
} else {
console.log("余额不足");
}
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(1000);
account.deposit(500); // 存入 500,余额:1500
account.withdraw(200); // 取出 200,余额:1300
console.log(account.getBalance()); // 1300
// ❌ 无法直接修改 balance
// account.balance = 999999; // 无效
优势:
- ✅ 保护数据不被外部随意修改
- ✅ 提供受控的访问接口
2. 函数柯里化(Currying)
将多参数函数转换为一系列单参数函数。
javascript
// 普通函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化版本
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
// 使用
console.log(curriedAdd(1)(2)(3)); // 6
// 可以部分应用
const addFive = curriedAdd(5);
const addFiveAndTen = addFive(10);
console.log(addFiveAndTen(15)); // 30
实际应用场景:
javascript
// 创建特定类型的验证器
function createValidator(type) {
return function (value) {
switch (type) {
case "email":
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
case "phone":
return /^1[3-9]\d{9}$/.test(value);
case "idCard":
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value);
default:
return false;
}
};
}
const validateEmail = createValidator("email");
const validatePhone = createValidator("phone");
console.log(validateEmail("test@example.com")); // true
console.log(validatePhone("13800138000")); // true
3. 防抖(Debounce)与节流(Throttle)
防抖函数
javascript
function debounce(func, delay) {
let timer = null;
return function (...args) {
// 清除上一次的定时器
if (timer) {
clearTimeout(timer);
}
// 重新设置定时器
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用场景:搜索框输入
const searchInput = document.getElementById("search");
searchInput.addEventListener(
"input",
debounce(function (e) {
console.log("搜索:", e.target.value);
// 发起搜索请求
}, 300),
);
节流函数
javascript
function throttle(func, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
func.apply(this, args);
}
};
}
// 使用场景:滚动事件
window.addEventListener(
"scroll",
throttle(function () {
console.log("滚动位置:", window.scrollY);
}, 200),
);
4. 模块化模式
javascript
const CounterModule = (function () {
let count = 0; // 私有变量
function increment() {
count++;
console.log(`当前计数:${count}`);
}
function decrement() {
count--;
console.log(`当前计数:${count}`);
}
function getCount() {
return count;
}
// 只暴露公共接口
return {
increment,
decrement,
getCount,
};
})();
// 使用
CounterModule.increment(); // 当前计数:1
CounterModule.increment(); // 当前计数:2
CounterModule.decrement(); // 当前计数:1
console.log(CounterModule.getCount()); // 1
// ❌ 无法直接访问 count
// console.log(CounterModule.count); // undefined
5. 迭代器和生成器
javascript
function createIterator(array) {
let index = 0;
return {
next() {
if (index < array.length) {
return {
value: array[index++],
done: false,
};
} else {
return { done: true };
}
},
};
}
const iterator = createIterator([1, 2, 3, 4, 5]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
⚠️ 闭包的缺点
1. 内存占用增加
闭包会保持对外部变量的引用,导致这些变量无法被垃圾回收。
javascript
function createBigClosure() {
const largeArray = new Array(1000000).fill("data"); // 大数组
return function () {
console.log(largeArray.length); // 引用了 largeArray
};
}
const closure = createBigClosure();
// largeArray 无法被回收,即使不再需要
2. 性能问题
频繁创建闭包会影响性能。
javascript
// ❌ 不好的做法 - 每次循环都创建新闭包
for (let i = 0; i < 10000; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// ✅ 好的做法 - 使用 let 块级作用域
for (let i = 0; i < 10000; i++) {
setTimeout(() => {
console.log(i);
}, 100);
}
3. 调试困难
闭包可能导致变量作用域复杂,难以追踪。
javascript
function outer() {
let x = 10;
function middle() {
let y = 20;
function inner() {
let z = 30;
console.log(x + y + z); // 需要追踪三个作用域
}
return inner;
}
return middle;
}
4. 意外的副作用
javascript
// 经典面试题
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出 5 个 5
}, 100);
}
// 原因:var 没有块级作用域,所有闭包共享同一个 i
🛡️ 如何防止内存泄漏?
什么是内存泄漏?
当闭包引用的变量不再需要,但仍然存在于内存中,无法被垃圾回收时,就发生了内存泄漏。
常见场景及解决方案
场景一:全局变量引用
javascript
// ❌ 不好的做法
let globalData = null;
function processData() {
const largeData = new Array(1000000).fill("data");
globalData = function () {
console.log(largeData.length);
};
}
processData();
// largeData 永远不会被回收
// ✅ 好的做法
function processData() {
const largeData = new Array(1000000).fill("data");
const result = largeData.length;
// 使用完后立即释放
return function () {
console.log(result); // 只保留需要的数据
};
}
const handler = processData();
handler();
handler = null; // 手动解除引用
场景二:DOM 事件监听器
javascript
// ❌ 不好的做法 - 忘记移除事件监听器
function setupListener() {
const element = document.getElementById("myButton");
const data = new Array(1000000).fill("data");
element.addEventListener("click", function () {
console.log(data.length); // 引用了大数据
});
}
setupListener();
// 即使元素被移除,data 也无法回收
// ✅ 好的做法 - 及时清理事件监听器
function setupListener() {
const element = document.getElementById("myButton");
const data = new Array(1000000).fill("data");
function handleClick() {
console.log(data.length);
}
element.addEventListener("click", handleClick);
// 提供清理函数
return function cleanup() {
element.removeEventListener("click", handleClick);
};
}
const cleanup = setupListener();
// 当不需要时调用
cleanup();
场景三:定时器未清除
javascript
// ❌ 不好的做法
function startTimer() {
const data = {
/* 大量数据 */
};
setInterval(function () {
console.log(data);
}, 1000);
}
startTimer();
// 定时器永远运行,data 无法回收
// ✅ 好的做法
function startTimer() {
const data = {
/* 大量数据 */
};
const timerId = setInterval(function () {
console.log(data);
}, 1000);
// 返回清理函数
return function stopTimer() {
clearInterval(timerId);
};
}
const stopTimer = startTimer();
// 当不需要时停止
setTimeout(stopTimer, 5000);
场景四:Vue/React 组件中的闭包
javascript
// Vue 示例
export default {
mounted() {
const largeData = new Array(1000000).fill("data");
// ❌ 忘记在 beforeDestroy 中清理
this.timer = setInterval(() => {
console.log(largeData);
}, 1000);
},
// ✅ 正确的做法
beforeUnmount() {
// Vue 3
// beforeDestroy() { // Vue 2
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
};
// React 示例
function MyComponent() {
useEffect(() => {
const largeData = new Array(1000000).fill("data");
const timer = setInterval(() => {
console.log(largeData);
}, 1000);
// ✅ 返回清理函数
return () => {
clearInterval(timer);
};
}, []);
return <div>My Component</div>;
}
场景五:缓存管理
javascript
// ❌ 不好的做法 - 无限增长的缓存
const cache = {};
function getData(id) {
if (cache[id]) {
return cache[id];
}
const data = fetchLargeData(id); // 获取大数据
cache[id] = data; // 永久保存
return data;
}
// ✅ 好的做法 - 使用 WeakMap 或限制缓存大小
class CacheManager {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
return this.cache.get(key);
}
set(key, value) {
// 如果超过最大容量,删除最旧的条目
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
}
const cache = new CacheManager(100);
通用防内存泄漏策略
1. 及时解除引用
javascript
let data = loadData(); // 加载大数据
// 使用数据
processData(data);
// ✅ 使用完后立即释放
data = null;
2. 使用弱引用(WeakMap/WeakSet)
javascript
// WeakMap 的键是弱引用,可以被垃圾回收
const weakCache = new WeakMap();
function associateData(obj, data) {
weakCache.set(obj, data);
}
function getData(obj) {
return weakCache.get(obj);
}
// 当 obj 被回收时,对应的数据也会被回收
3. 避免不必要的全局变量
javascript
// ❌ 不好的做法
window.myAppData = {
/* 大量数据 */
};
// ✅ 好的做法 - 使用模块或闭包封装
(function () {
const myAppData = {
/* 大量数据 */
};
// 只暴露必要的接口
window.myApp = {
getData() {
return myAppData;
},
};
})();
4. 定期检查和清理
javascript
// 定期检查内存使用情况
if (window.performance && window.performance.memory) {
setInterval(() => {
const memory = window.performance.memory;
console.log("已用内存:", memory.usedJSHeapSize);
console.log("总内存:", memory.totalJSHeapSize);
// 如果内存使用过高,触发清理
if (memory.usedJSHeapSize > memory.totalJSHeapSize * 0.8) {
console.warn("内存使用过高,建议清理");
// 执行清理操作
cleanupUnusedResources();
}
}, 30000); // 每 30 秒检查一次
}
5. 使用 Chrome DevTools 检测内存泄漏
步骤:
- 打开 Chrome DevTools
- 切换到 Memory 面板
- 选择 Heap snapshot
- 拍摄快照
- 执行操作
- 再次拍摄快照
- 比较两个快照,查找未释放的对象
💼 项目中的实际应用
应用一:API 请求封装
javascript
// utils/request.js
function createRequest(baseURL) {
const defaultHeaders = {
"Content-Type": "application/json",
};
// 拦截器
const interceptors = {
request: [],
response: [],
};
async function request(url, options = {}) {
const fullUrl = `${baseURL}${url}`;
// 执行请求拦截器
let config = {
...options,
headers: { ...defaultHeaders, ...options.headers },
};
for (const interceptor of interceptors.request) {
config = await interceptor(config);
}
try {
const response = await fetch(fullUrl, config);
let data = await response.json();
// 执行响应拦截器
for (const interceptor of interceptors.response) {
data = await interceptor(data);
}
return data;
} catch (error) {
console.error("请求失败:", error);
throw error;
}
}
// 添加拦截器
request.addRequestInterceptor = function (interceptor) {
interceptors.request.push(interceptor);
};
request.addResponseInterceptor = function (interceptor) {
interceptors.response.push(interceptor);
};
return request;
}
// 使用
const api = createRequest("https://api.example.com");
// 添加 token 拦截器
api.addRequestInterceptor((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 添加错误处理拦截器
api.addResponseInterceptor((data) => {
if (data.code !== 200) {
throw new Error(data.message);
}
return data.data;
});
// 发起请求
api("/users").then((users) => {
console.log(users);
});
应用二:状态管理(简化版 Redux)
javascript
// store/index.js
function createStore(reducer, initialState) {
let state = initialState;
const listeners = [];
function getState() {
return state;
}
function dispatch(action) {
state = reducer(state, action);
// 通知所有订阅者
listeners.forEach((listener) => listener());
}
function subscribe(listener) {
listeners.push(listener);
// 返回取消订阅函数
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
return {
getState,
dispatch,
subscribe,
};
}
// 使用
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
};
const store = createStore(reducer);
// 订阅状态变化
const unsubscribe = store.subscribe(() => {
console.log("状态变化:", store.getState());
});
store.dispatch({ type: "INCREMENT" }); // 状态变化: { count: 1 }
store.dispatch({ type: "INCREMENT" }); // 状态变化: { count: 2 }
// 取消订阅
unsubscribe();
应用三:路由守卫
javascript
// router/guard.js
function createRouterGuard(router) {
const guards = {
beforeEach: [],
afterEach: [],
};
// 注册前置守卫
function beforeEach(guard) {
guards.beforeEach.push(guard);
}
// 注册后置守卫
function afterEach(guard) {
guards.afterEach.push(guard);
}
// 执行路由跳转
async function navigate(to, from) {
// 执行前置守卫
for (const guard of guards.beforeEach) {
const result = await guard(to, from);
if (result === false) {
return false; // 阻止跳转
}
}
// 执行路由跳转
router.push(to);
// 执行后置守卫
guards.afterEach.forEach((guard) => {
guard(to, from);
});
return true;
}
return {
beforeEach,
afterEach,
navigate,
};
}
// 使用
const guard = createRouterGuard(router);
// 登录验证
guard.beforeEach((to, from) => {
const token = localStorage.getItem("token");
if (to.meta.requiresAuth && !token) {
// 未登录,跳转到登录页
router.push("/login");
return false;
}
return true;
});
// 记录页面访问
guard.afterEach((to, from) => {
console.log(`从 ${from.path} 跳转到 ${to.path}`);
});
🔧 常见面试题解析
Q1: 下面代码输出什么?
javascript
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
A : 输出 5 个 5
原因 :var 没有块级作用域,所有闭包共享同一个 i。当定时器执行时,循环已经结束,i 的值为 5。
解决方案:
javascript
// 方案一:使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 0, 1, 2, 3, 4
}, 100);
}
// 方案二:使用 IIFE
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 0, 1, 2, 3, 4
}, 100);
})(i);
}
Q2: 如何实现一个 once 函数,只执行一次?
javascript
function once(fn) {
let executed = false;
let result;
return function (...args) {
if (!executed) {
executed = true;
result = fn.apply(this, args);
}
return result;
};
}
// 使用
const initialize = once(function () {
console.log("初始化...");
return "initialized";
});
initialize(); // "初始化..."
initialize(); // 无输出
initialize(); // 无输出
Q3: 闭包会导致内存泄漏吗?
A : 不一定。闭包本身不会导致内存泄漏,只有当闭包引用的变量不再需要但无法被回收时,才会造成内存泄漏。
关键:
- ✅ 合理使用闭包是正常的
- ❌ 忘记清理不需要的引用才会导致泄漏
Q4: 如何判断是否存在内存泄漏?
A:
- 使用 Chrome DevTools 的 Memory 面板
- 观察内存使用是否持续增长
- 进行堆快照对比
- 检查是否有未清理的定时器、事件监听器等
📊 闭包 vs 其他方案对比
| 特性 | 闭包 | Class | Module |
|---|---|---|---|
| 数据私有性 | ✅ | ⚠️(需约定) | ✅ |
| 内存占用 | ⚠️(需注意) | ✅ | ✅ |
| 代码可读性 | ⚠️ | ✅ | ✅ |
| 适用场景 | 小型工具函数 | 复杂对象 | 大型模块 |
🎓 进阶知识
闭包的性能优化
javascript
// ❌ 不好的做法 - 每次都创建新函数
function createGreeter(name) {
return function () {
console.log(`Hello, ${name}!`);
};
}
// ✅ 好的做法 - 缓存函数
const greeterCache = new Map();
function getGreeter(name) {
if (!greeterCache.has(name)) {
greeterCache.set(name, function () {
console.log(`Hello, ${name}!`);
});
}
return greeterCache.get(name);
}
尾调用优化
javascript
// ❌ 可能导致栈溢出
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// ✅ 尾递归优化(需要引擎支持)
function factorialTail(n, accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator);
}
📚 参考资源
🙋 常见问题
Q1: 箭头函数有闭包吗?
A: 有!箭头函数同样可以形成闭包。
javascript
function outer() {
const name = "张三";
return () => {
console.log(name); // 形成闭包
};
}
Q2: 闭包和回调函数有什么区别?
A:
- 闭包:是一种机制,函数可以访问其词法作用域
- 回调函数:是一种编程模式,作为参数传递的函数
- 回调函数可以是闭包,但不一定是
Q3: 如何调试闭包?
A:
- 使用 Chrome DevTools 的 Sources 面板
- 在闭包函数中设置断点
- 查看 Scope 面板中的作用域链
- 观察 Closure 下的变量
🎉 结语
闭包是 JavaScript 的强大特性,正确使用它可以:
- ✅ 实现数据封装和私有变量
- ✅ 创建灵活的函数工厂
- ✅ 实现防抖、节流等实用功能
- ✅ 构建模块化架构
但也要注意:
- ⚠️ 避免不必要的内存占用
- ⚠️ 及时清理不需要的引用
- ⚠️ 注意性能影响
记住三个原则:
- 按需使用:只在必要时使用闭包
- 及时清理:不再使用时解除引用
- 监控内存:定期检查内存使用情况
掌握闭包,让你的 JavaScript 代码更加优雅和高效!
💬 互动时间:你在项目中如何使用闭包的?遇到过哪些内存泄漏问题?欢迎在评论区分享你的经验!
如果觉得这篇文章对你有帮助,请点赞👍、收藏⭐、转发🔄,让更多人受益!