终于理解闭包是什么了(含防抖、节流)
闭包(Closure)是编程中一个非常重要的概念,尤其在 JavaScript 中广泛使用。它的核心是函数和其周围状态(词法环境)的组合,可以简单理解为“函数记住了自己被创建时的环境”。
闭包的核心定义
- 闭包是一个函数,它可以访问并记住自己定义时的作用域(即使该作用域已经执行完毕)。
- 本质:函数内部嵌套的函数,能够“捕获”外部函数的变量,并长期保留这些变量的引用。
用「背包」来类比
假设你是一个学生,每天背着书包去上学。闭包就像你的书包:
- 书包里有什么? 装着你需要的课本、文具(相当于函数内部需要的变量)。
- 书包的规则: 你可以在教室里(函数内部)往书包里放东西,即使放学回家了(函数执行完毕) ,书包里的东西依然存在,第二天还能继续用。
- 关键点: 书包里的东西(变量)只属于你,别人拿不到(私有性)。
闭包的本质: 函数放学回家了(执行完毕),但它依然背着自己的书包(保存了函数创建时的作用域变量),随时可以打开书包用里面的东西。
举个直观的例子 🌰
function outer() {
const name = "Alice"; // outer 函数的局部变量
function inner() {
console.log(name); // inner 函数访问 outer 的变量
}
return inner; // 返回 inner 函数
}
const myFunc = outer(); // outer 执行完毕,按理说 name 应该被销毁
myFunc(); // 输出 "Alice" ✅——闭包让 inner 记住了 name 的值!
发生了什么?
outer()
执行后,其作用域按理应该被销毁。- 但
inner
函数被返回,且它引用了outer
中的变量name
。 - 闭包保留了
name
** 的引用**,因此myFunc()
仍然能访问到name
。
闭包的三大特性
- 访问外部作用域:内部函数可以访问外部函数的变量。
- 灵活控制: 可以用闭包动态生成不同功能的小工具(如多个独立计数器)。
- 变量长期存活:即使外部函数已执行完毕,其变量也不会被垃圾回收。
- 记住过去: 函数执行后,依然能记住之前的状态(比如游戏存档)。
- 变量私有化:闭包中的变量对外部不可见,实现数据封装。
- 保护隐私: 像你的日记本,只有你自己能修改(封装私有变量)。
闭包的经典应用场景
1. 模块化开发(封装私有变量)
function createCounter() {
let count = 0; // 私有变量,外部无法直接访问
return {
increment: () => { count++; },
getCount: () => { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
- 优势:
count
被保护在闭包内,只能通过暴露的方法修改,避免全局污染。
javascript 体验AI代码助手 代码解读复制代码 // 计数器(你的小金库)
function 创建小金库() {
let 余额 = 0;
// 藏在闭包里的钱,外部无法直接修改
return {
存钱: (金额) => { 余额 += 金额; },
查余额: () => { return 余额; }};
}
const 我的小金库 = 创建小金库();
我的小金库.存钱(100);
console.log(我的小金库.查余额()); // 100 ✅
- 闭包的作用: 把
余额
藏在书包里,只通过特定方法操作,防止别人偷钱(保护数据)。
2. 回调函数(保留上下文)
javascript 体验AI代码助手 代码解读复制代码function fetchData(url) {
let data = null;
// 模拟异步请求
setTimeout(() => {
data = "响应数据";
}, 1000);
return {
then: (callback) => {
setTimeout(() => {
callback(data); // 回调函数通过闭包访问 data
}, 1000);
}
};
}
fetchData("https://api.example.com")
.then((data) => console.log(data)); // 1秒后输出 "响应数据"
javascript 体验AI代码助手 代码解读复制代码// 记住你的偏好(比如网页主题)
function 主题切换器(初始主题) {
let 当前主题 = 初始主题; // 藏在闭包里的主题
return {
切换主题: () => {
当前主题 = 当前主题 === 'light' ? 'dark' : 'light';
document.body.style.background = 当前主题 === 'dark' ? '#333' : '#fff';
}
};
}
const 切换按钮 = 主题切换器('light');
document.getElementById('themeBtn').addEventListener('click', 切换按钮.切换主题);
//闭包的作用: 点击按钮时,函数依然记得 当前主题 的值(即使函数已经执行完),实现状态持久化。
3. 防抖(Debounce)和节流(Throttle)
防抖 ——「电梯关门」的哲学
想象你在电梯里:
- 规则:电梯门打开后,如果 10秒内 有人按开门按钮,电梯会重新计时10秒;直到 连续10秒没人按按钮,电梯才会关门。
- 本质:只响应最后一次连续操作,避免频繁触发。
javascript 体验AI代码助手 代码解读复制代码 // 代码示例(搜索框输入)
// 闭包的作用: 用闭包保存 timer 变量,让多次触发的回调函数共享同一个计时器(类似电梯记住倒计时状态)。
function debounce(func, delay) {
let timer; // 藏在闭包里的计时器(电梯的倒计时)
return function(...args) {
clearTimeout(timer); // 每次输入都重置计时(类似按开门按钮)
timer = setTimeout(() => {
func.apply(this, args); // 延迟结束后执行搜索(电梯关门)
}, delay);
};
}
const 输入框 = document.getElementById('search');
输入框.addEventListener('input', debounce(() => {
console.log('发送搜索请求...'); // 只在用户停止输入后触发
}, 500));
节流 ——「水龙头滴水」的哲学
想象你拧开水龙头:
- 规则:无论你拧得多快,水龙头 每秒最多滴一滴水,多余的触发被忽略。
- 本质:固定时间间隔内只触发一次,稀释触发频率。
javascript 体验AI代码助手 代码解读复制代码// 代码示例(窗口滚动事件)
// 闭包的作用:用闭包保存 lastTime 变量,记录上一次执行时间,让多次触发的回调函数共享这个时间戳。
function throttle(func, interval) {
let lastTime = 0; // 藏在闭包里的上一次执行时间(记录上一次滴水时间)
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) { // 距离上次执行超过间隔时间
func.apply(this, args); // 执行函数(滴一滴水)
lastTime = now; // 更新记录时间
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('处理滚动逻辑...'); // 每 200ms 最多触发一次
}, 200));
防抖 vs 节流的区别
场景 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
核心思想 | 事件停止后才触发 | 固定间隔触发一次 |
类比 | 电梯关门 | 水龙头滴水 |
典型应用 | 搜索框输入、窗口 resize 结束 | 滚动事件、鼠标移动、射击游戏连发 |
代码差异 | 每次触发重置计时器 | 根据时间间隔判断是否执行 |
闭包的关键作用
- 状态持久化:防抖和节流需要记住
timer
或lastTime
,这些变量必须在多次函数调用间共享。 - 变量私有化:避免将
timer
或lastTime
暴露在全局,防止污染其他代码(类似电梯的倒计时器只能由电梯自己控制)。
如果没有闭包会怎样?
javascript 体验AI代码助手 代码解读复制代码// 没有闭包时,只能将计时器存在全局
let globalTimer; // 💥 全局变量,多个防抖函数会互相覆盖
function debounce(func, delay) {
return function() {
clearTimeout(globalTimer); // 所有防抖共享同一个计时器
globalTimer = setTimeout(func, delay);
};
}
// 页面中有两个输入框:
const input1 = document.getElementById('input1');
const input2 = document.getElementById('input2');
input1.addEventListener('input', debounce(() => {
console.log('搜索框1的请求');
}, 500));
input2.addEventListener('input', debounce(() => {
console.log('搜索框2的请求');
}, 500));
// 问题:当两个输入框同时触发时,它们会互相清除对方的计时器!
// 结果:只有一个请求会被发送,另一个被取消。
javascript 体验AI代码助手 代码解读复制代码// 没有闭包时,只能将上一次执行时间存在全局
let globalLastTime = 0; // 💥 所有节流共享同一个时间戳
function throttle(func, interval) {
return function() {
const now = Date.now();
if (now - globalLastTime >= interval) {
func();
globalLastTime = now;
}
};
}
// 页面中有两个需要节流的操作:
window.addEventListener('scroll', throttle(() => {
console.log('处理滚动逻辑');
}, 200));
document.addEventListener('mousemove', throttle(() => {
console.log('处理鼠标移动');
}, 200));
// 问题:滚动和鼠标移动共享同一个时间戳!
// 结果:滚动触发后,鼠标移动的逻辑会被强制延迟,反之亦然。
闭包的潜在问题
1. 内存泄漏
javascript 体验AI代码助手 代码解读复制代码function leakMemory() {
const bigData = new Array(1000000).fill("⚠️"); // 大数据
return function() {
console.log("闭包引用了 bigData,导致它无法被回收!");
};
}
const leakedFunc = leakMemory(); // bigData 一直被闭包引用,无法释放
- 解决方法:在不需要时手动解除引用(如
leakedFunc = null
)。
javascript 体验AI代码助手 代码解读复制代码// 内存泄漏: 如果书包里装了大石头(大数据),又不扔,书包会越来越重(内存占用)。
function 装石头() {
const 大石头 = new Array(1000000).fill("重");
return () => { console.log(大石头[0]); };
}
const 沉重的书包 = 装石头(); // 大石头一直存在内存中!
// 解决: 不用时把书包置空(沉重的书包 = null)。
2. 意外的闭包(常见于循环)
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 3 次 3 ❗(var 没有块级作用域)
}, 100);
}
-
解决:用
let
替代var
(利用块级作用域),或使用闭包隔离:-
for (var i = 0; i < 3; i++) { (function(j) { // 立即执行函数创建新作用域 setTimeout(() => { console.log(j); // 输出 0, 1, 2 ✅ }, 100); })(i); }
-
闭包与其他语言
- JavaScript:闭包是核心特性,天然支持。
- Python/Go/Rust:支持闭包,但可能需要显式声明捕获变量。
- Java/C++ :通过匿名内部类或 Lambda 表达式模拟闭包,限制较多。
总结
- 闭包的核心:函数 + 定义时的词法环境。
- 优点:封装数据、模块化开发、保留上下文。
- 注意事项:避免内存泄漏、正确处理循环中的闭包。
- 哲学:闭包是“时间胶囊”,让函数穿越时空,记住过去的状态