昨天我们认识了闭包------那个"虽然离开了家,但还记得家里密码"的神奇函数。今天咱们来深挖一下:闭包这玩意儿到底能干啥?有没有什么副作用?怎么防止它把内存吃光?看完这篇,你不仅知道闭包怎么用,还能在面试官面前侃侃而谈。
前言
闭包就像一个"赖着不走"的租客。你以为人走了,结果他还留着你的钥匙,时不时回来拿点东西。这在JavaScript里有时候特别好用,有时候又特别坑。
今天我们就来盘点闭包的几个经典应用场景,顺便聊聊怎么让它"体面退场",别把你的内存吃光。
一、闭包的应用场景:这个"赖着不走"的家伙还挺有用
1. 模块化:私有变量与公共方法
没有ES6模块之前,闭包是JS实现模块化的主要手段。它能把内部细节藏起来,只暴露需要公开的接口。
js
const counter = (function() {
let count = 0; // 私有变量,外面访问不到
function increment() {
count++;
console.log(count);
}
function decrement() {
count--;
console.log(count);
}
function getCount() {
return count;
}
return {
increment,
decrement,
getCount
};
})();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.count); // undefined,拿不到
console.log(counter.getCount()); // 2
这个模式叫IIFE(立即执行函数) ,它创建了一个闭包,里面的count变量被返回的方法"记住"了,外部无法直接修改,只能通过提供的接口操作。像不像一个"保险箱"?钥匙只给了你几个特定的人。
2. 函数工厂:批量生产定制函数
闭包可以用来创建带有特定"预设"的函数,比如一个能记录调用次数的函数。
js
function createCounter(initial = 0) {
let count = initial;
return function() {
count++;
return count;
};
}
const counterA = createCounter(10);
console.log(counterA()); // 11
console.log(counterA()); // 12
const counterB = createCounter(0);
console.log(counterB()); // 1
每个计数器都独立拥有自己的count变量,互不干扰。这个工厂就像是做定制蛋糕,每个客户拿到的是自己专属的那一份。
3. 防抖与节流:控制函数执行频率
防抖和节流是前端性能优化的常见手法,它们的核心都依赖闭包来保存计时器和状态。
防抖:用户连续触发事件时,只有最后一次等待结束后才执行(比如搜索框输入)。
js
function debounce(fn, delay) {
let timer = null; // 闭包保存timer
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用
const search = debounce(() => console.log('搜索中...'), 500);
节流:限制函数在单位时间内最多执行一次(比如滚动事件)。
js
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
这两个函数返回的都是闭包,里面的timer或last被"记住"了,所以每次调用都能访问到上一次的状态。
4. 柯里化:提前固定参数
柯里化是把多参数函数变成一系列单参数函数的技术,本质也是闭包。
js
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...more) {
return curried.apply(this, args.concat(more));
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
每次返回新函数时,原来的args被闭包保存,直到参数凑齐才执行。就像是你给一家餐厅留了订单,每次打电话加菜,最后一起结算。
5. 事件监听中的回调
在事件回调里访问外部变量,其实也是闭包。比如一个简单的计数器按钮:
js
let count = 0;
document.getElementById('btn').addEventListener('click', function() {
count++;
console.log(count);
});
这里的匿名函数"记住"了外部的count变量,每次点击都能访问到最新的值。
二、闭包的"阴暗面":内存泄漏与性能
闭包这么香,为什么还有人说它不好?因为它会"赖着不走"------那些被记住的变量,即使外部函数已经执行完了,也不会被垃圾回收,只要闭包函数还活着,它们就一直存在。
1. 什么是内存泄漏?
内存泄漏就是程序用完了内存,但系统没有及时回收,导致内存占用越来越大,最后浏览器变卡、甚至崩溃。
闭包导致泄漏的典型场景:
js
function leak() {
let bigData = new Array(1000000).fill('leak');
return function() {
console.log('I am a closure');
// 虽然没有直接使用bigData,但闭包还是引用了它
};
}
const closureFn = leak(); // 泄漏了100万个元素的数组
上面这个例子中,返回的函数虽然没有用到bigData,但因为bigData和它在同一个作用域,闭包会保留整个作用域链上的所有变量。所以如果闭包一直存在,那些无用的变量也一直占用内存。
2. 如何避免闭包导致的内存泄漏?
- 用完后解除引用 :把闭包函数的变量置为
null。
js
closureFn = null; // 这样bigData就可以被回收了
- 只保留需要的变量 :如果闭包中只用到部分变量,可以用
let声明在闭包外部提前"过滤"。
js
function good() {
let bigData = new Array(1000000).fill('data');
let needed = 'only me';
return function() {
console.log(needed); // 只引用needed,bigData会被回收
};
}
因为闭包只引用了needed,引擎可以优化,把bigData标记为不可达。
- 避免在循环中创建闭包(除非必要),因为循环中的闭包可能会意外持有大量变量。
3. 弱引用:救星Map和Set
ES6引入了WeakMap和WeakSet,它们的键是弱引用的------如果键对象不再被其他地方引用,那么即使还在WeakMap里,也会被垃圾回收。
这在闭包中可以用来缓存数据,而不阻止回收。
js
const cache = new WeakMap();
function process(obj) {
if (!cache.has(obj)) {
const result = heavyComputation(obj);
cache.set(obj, result);
}
return cache.get(obj);
}
如果obj在其他地方被销毁了,cache里的键值对也会自动消失,不会造成泄漏。
三、实战:闭包的最佳实践
-
用闭包封装私有数据 :在不需要完全隔离的情况下,闭包是模块化的好帮手。但现代开发可以用ES6模块(
import/export)替代IIFE,更清晰。 -
防抖节流用闭包保存状态:这是闭包的经典应用,没啥好纠结的。
-
谨慎使用返回闭包的高阶函数:如果闭包持有大量数据,确保及时清理。
-
善用
let替代var:let有块级作用域,能避免一些意外的闭包问题。 -
在DevTools里监控内存:用Chrome的Memory面板,可以拍快照,看看哪些闭包对象一直存在,帮助定位泄漏。
四、总结:闭包是个好员工,但别让它996
闭包是JavaScript的强大特性,它让函数拥有了"记忆",能实现模块化、柯里化、防抖节流等高级功能。但也要注意它的副作用:被记住的变量不会自动消失,如果不注意,容易造成内存泄漏。
记住几个原则:
- 用完闭包,及时解除引用。
- 在闭包里只引用需要的变量,减少内存占用。
- 现代开发中,能用ES6模块就用模块,减少手动闭包模式。
- 遇到缓存场景,优先考虑
WeakMap。
掌握了闭包,你就掌握了JS高级编程的核心钥匙。明天我们将走进JS的另一个灵魂领域------原型和原型链,看看那个让新手望而生畏的概念,到底是怎么一回事。
如果你觉得今天的闭包应用和内存管理讲得透彻,点个赞让更多人看到。有疑问评论区见,我们明天见!