先看一个现象:
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
- 注意事项:大对象闭包引用可能导致内存泄漏