一、闭包的定义
闭包(Closure) 是指 有权访问另一个函数作用域中的变量的函数 。
简单来说,就是函数可以"记住"定义时的作用域,而不是调用时的作用域。
scss
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
}
}
const fn = outer();
fn(); // 1
fn(); // 2
这里
inner就是闭包,它可以访问outer的变量count。
二、闭包的形成条件
闭包产生通常需要三个条件:
- 函数嵌套:有函数内部函数。
- 内部函数引用外部变量。
- 外部函数返回内部函数或将内部函数传出。
三、闭包的作用
- 保护变量私有化(封装)
javascript
function createCounter() {
let count = 0;
return {
increment() { count++; return count; },
decrement() { count--; return count; }
}
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.decrement()); // 0
外部无法直接访问
count,只能通过提供的方法操作。
- 函数工厂 / 动态生成函数
javascript
function multiplyBy(n) {
return function(x) {
return x * n;
}
}
const double = multiplyBy(2);
console.log(double(5)); // 10
- 保持状态(常用于异步、事件回调)
四、闭包的面试常考点
1. 循环中的闭包问题
javascript
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
// 输出:3 3 3
原因 :var 是函数作用域,闭包引用的是同一个 i。
解决方案:
- 使用
let(块级作用域):
javascript
for (let i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
// 输出:0 1 2
- 或使用立即执行函数(IIFE):
css
for (var i = 0; i < 3; i++) {
(function(i){
setTimeout(function() { console.log(i); }, 100);
})(i);
}
2. 闭包与内存泄漏
闭包会 引用外部作用域变量 ,可能导致 垃圾回收无法释放。
- 避免过度使用全局闭包。
- 在不需要时,手动清理引用。
3. 面试题示例
题1:输出结果?
ini
function makeFunc() {
let name = "Yo Yo";
return function() {
console.log(name);
}
}
const func = makeFunc();
func();
答案 :Yo Yo
因为闭包保持了对
name的引用。
题2:循环闭包输出?
scss
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function() { console.log(i); };
}
funcs[0](); funcs[1](); funcs[2]();
答案 :3 3 3
var是函数作用域,闭包引用的是同一个i。
题3:使用闭包实现私有变量
javascript
function Counter() {
let count = 0;
this.increment = function() { count++; return count; }
this.decrement = function() { count--; return count; }
}
const c = new Counter();
console.log(c.increment()); // 1
题4:闭包结合 setTimeout
css
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
答案 :0 1 2
let作用域绑定每次循环的i。
五、闭包引起内存泄漏问题
1 为什么闭包会造成内存无法释放?
闭包之所以"强大",是因为它让内部函数持续引用 外部作用域中的变量。
但也正因为这个引用存在,即使外层函数已经执行完毕,外层变量仍然被保存在内存中,不能被垃圾回收(GC)释放。
示例
javascript
function outer() {
let bigData = new Array(1000000).fill('数据');
return function inner() {
console.log(bigData.length);
}
}
const fn = outer();
// outer 已经执行完毕,但 bigData 仍被 inner 引用,无法回收
这里
bigData占用大量内存,而fn一直持有引用,GC 就不会释放。
2 在不需要时,如何"手动清理引用"
关键思路:让闭包不再引用外部变量。
✅ 方式 1:将闭包变量置为 null
javascript
function outer() {
let bigData = new Array(1000000).fill('数据');
return function inner() {
console.log(bigData.length);
bigData = null; // 手动解除引用
}
}
const fn = outer();
fn(); // 使用一次
调用
fn后手动将bigData = null,让 GC 能识别为可回收。
✅ 方式 2:将闭包函数引用置为 null
php
let fn = (function() {
let data = { name: 'Yo Yo' };
return function() {
console.log(data);
}
})();
fn(); // 使用
fn = null; // 手动解除引用,闭包随之释放
一旦
fn没有被引用,整个闭包作用域都会被 GC。
✅ 方式 3:避免不必要的全局闭包
不要让闭包函数挂在全局对象上,比如:
javascript
window.fn = (function() {
let cache = [];
return function() { cache.push(Date.now()); }
})();
改进:
javascript
(function() {
let cache = [];
document.body.addEventListener('click', function() {
cache.push(Date.now());
});
})();
只在局部使用闭包,事件解绑或模块销毁时自然会释放。
✅ 方式 4:组件或页面卸载时清理(在框架中)
比如 React、Vue 中使用闭包保存状态时,在组件卸载阶段清理:
React 示例
javascript
useEffect(() => {
let timer = setInterval(() => console.log('run'), 1000);
return () => clearInterval(timer); // 清理引用
}, []);
如果闭包里引用了外部变量(如 DOM、定时器),必须在 cleanup 阶段清理,否则内存会持续增长。
3、判断闭包导致的内存泄漏的常见迹象
- 控制台内存面板中,执行某函数后内存不下降;
- 页面关闭某功能后,内存占用仍持续增加;
- 浏览器 Performance 记录中,Detached DOM 节点未被释放。
总结要点
-
闭包 = 函数 + 对外部变量的引用
-
好处:
- 数据私有化
- 保存状态
- 动态函数生成
-
注意:
- 循环变量问题
- 内存泄漏
- 不要滥用全局闭包