闭包到底有什么用?别只背概念,看完秒懂实际业务场景
如果你搜过"什么是闭包",大概率看到过这样的定义:
"闭包是指有权访问另一个函数作用域中的变量的函数。"
说实话,第一次看到这句话的时候,我脑子里只有三个字:这啥啊?
今天咱们不背概念,直接用真实业务场景告诉你:闭包到底解决了什么问题、在什么情况下你会主动用它。
场景一:计数器 ------ 不想让外部随便改数据
业务需求
做一个按钮点击计数器,要求:
- 每次点击数字 +1
- 外部不能直接把计数清零或篡改
❌ 不用闭包的做法(有坑)
ini
let count = 0;
function increment() {
count++;
console.log(count);
}
问题很明显:count 是个全局变量,任何人都能直接改:
ini
count = 0; // 随便清零,防不胜防
✅ 用闭包的做法
scss
function createCounter() {
let count = 0; // 私有变量,外部碰不到
return function () {
count++;
console.log(count);
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
为什么这是闭包?
createCounter 执行完后,它内部的 count 本该被销毁。但因为返回的函数引用了 count,JavaScript 引擎会把它保留在内存中 ------ 这就是闭包的核心机制。
实际价值:
count对外完全不可见,只能通过counter()修改- 这是 JS 中实现私有变量的唯一原生方式
场景二:防抖(Debounce)------ 搜索框输入不疯狂请求接口
业务需求
搜索框输入时调用接口,但用户打字很快,不能每敲一个字就请求一次,要等用户停下来再发请求。
完整实现
javascript
function debounce(fn, delay) {
let timer = null; // 闭包变量:记录定时器
return function (...args) {
clearTimeout(timer); // 每次触发都清除上一个定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用
const search = debounce((keyword) => {
console.log('请求接口:', keyword);
}, 500);
input.addEventListener('input', (e) => {
search(e.target.value);
});
为什么必须用闭包?
timer 需要跨多次函数调用来保存状态。如果不用闭包,你得把 timer 放在全局,那多个防抖函数就会互相干扰。
闭包在这里的作用:给每个 debounce 调用创建一个独立的"小房间",timer 住在里面,外面进不去。
场景三:函数工厂 ------ 批量生成相似但独立的功能
业务需求
页面上有三种按钮:普通用户、VIP、管理员,点击后弹出不同的提示语。
❌ 不用闭包的写法
ini
function handleClick(role) {
if (role === 'user') {
alert('欢迎普通用户');
} else if (role === 'vip') {
alert('欢迎尊贵的VIP');
} else if (role === 'admin') {
alert('管理员你好');
}
}
问题:每次点击都要判断角色,而且如果角色很多,这个函数会越来越臃肿。
✅ 用闭包的写法
ini
function createGreeting(role) {
const messages = {
user: '欢迎普通用户',
vip: '欢迎尊贵的VIP',
admin: '管理员你好'
};
return function () {
alert(messages[role]);
};
}
// 提前生成好三个函数
const greetUser = createGreeting('user');
const greetVip = createGreeting('vip');
const greetAdmin = createGreeting('admin');
// 绑定事件
userBtn.onclick = greetUser; // 点击直接弹对应消息
vipBtn.onclick = greetVip;
adminBtn.onclick = greetAdmin;
实际价值:
- 每个函数"记住"了自己的
role,点击时不需要再传参 - 代码更干净,性能更好(不用每次判断)
场景四:模块化封装 ------ 早期 JS 没有 class 时的"类"
在 ES6 之前,JS 没有 class 和 private,前端是怎么写模块的?靠闭包。
javascript
const UserModule = (function () {
let users = []; // 私有数据,外部无法直接访问
return {
add(name) {
users.push(name);
console.log(`${name} 已添加`);
},
list() {
return [...users]; // 返回副本,防止外部直接修改原数组
},
count() {
return users.length;
}
};
})();
UserModule.add('张三');
UserModule.add('李四');
console.log(UserModule.list()); // ['张三', '李四']
console.log(UserModule.count()); // 2
// users 外部完全访问不到
console.log(UserModule.users); // undefined
这就是早期 jQuery 插件、各种 JS 库的封装方式。 闭包让你可以模拟出"私有属性",这是公开变量做不到的。
场景五:循环 + 异步的经典坑(也是面试常客)
问题代码
css
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出什么?
}, 100);
}
很多人以为是 0, 1, 2,实际输出是:
3
3
3
原因:setTimeout 回调执行时,for 循环已经跑完了,i 的最终值是 3。
解决方案一:用闭包(IIFE)
javascript
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => {
console.log(j); // 0, 1, 2 ✅
}, 100);
})(i);
}
每次循环创建一个 IIFE(立即执行函数),j 通过闭包"锁住"了当时的 i 值。
解决方案二:用 let(本质也是闭包)
ini
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2 ✅
}, 100);
}
let 在每次迭代时都会创建一个新的块级作用域绑定,本质上就是引擎帮你做了闭包的事。
总结:闭包到底解决了什么问题?
| 场景 | 闭包解决的问题 |
|---|---|
| 计数器 / 私有变量 | 数据封装,防止外部随意篡改 |
| 防抖 / 节流 | 跨调用保存状态(定时器ID) |
| 函数工厂 | 让函数"记住"创建时的参数 |
| 模块封装 | 模拟私有属性和方法 |
| 循环 + 异步 | 锁定每次迭代的变量值 |
一句话记住闭包
闭包就是一个函数 + 它能访问的外部变量,打包在一起。
它的核心价值是:让数据活得比创建它的函数更久,同时对外隐藏。
不用死记硬背定义,下次写代码时想想:
"我需要一个函数记住某些数据,但不想让别人碰到这些数据。"
------ 这时候你就在用闭包了。