🤯 闭包:JavaScript 的“薛定谔的猫”,懂了就开悟,不懂就挠头!
今天我们要聊一个让无数前端萌新从入门到放弃,又让老鸟从放弃到真香的 JavaScript 核心概念——闭包(Closure) !
它就像薛定谔的猫:你不知道它有没有执行,但它已经在内存里“活”着了。 😼
别慌!今天我就用人话 + 代码段子,带你从“我是谁我在哪”到“我悟了!”的顿悟之旅。
准备好了吗?坐稳扶好,我们发车咯!🚀
🌪️ 第一章:闭包?不就是“包”嘛!
想象一下,你去奶茶店买了一杯芋圆波波奶茶🥤。
店员给你封了杯,盖了盖,还套了个袋子——这整个过程,就是“封装”。
而“闭包”,就是 JavaScript 的“奶茶封装术”!
🔐 什么是闭包?
官方定义:当一个函数能够访问其外部函数作用域中的变量时,就形成了闭包。
听起来像天书?来,上代码!
function outer() {
let secret = "我是你的小秘密~ 😘";
function inner() {
console.log(secret); // inner 能访问 outer 的变量
}
return inner; // 返回 inner 函数
}
const reveal = outer(); // 调用 outer,得到 inner
reveal(); // 输出:我是你的小秘密~ 😘
你看!outer 执行完后,按理说它的变量 secret 应该被销毁了。
但 inner 却依然能访问它——因为 inner “闭合”了 secret 这个变量,形成了闭包!
🎯 关键点:函数 + 外部变量的引用 = 闭包!
🧠 第二章:闭包的“超能力”——私有变量
在传统编程中,我们总想把某些东西藏起来,比如:
- 我的银行卡密码 💳
- 我的体重 🏋️♂️
- 我昨晚偷偷吃了三碗泡面 🍜
JavaScript 也一样!我们不想让外部随便修改内部变量。
于是——闭包登场!
function Book(title, author, year) {
let _title = title; // 私有变量,带下划线是约定
let _author = author;
let _year = year;
// 公共方法,可以访问私有变量
this.getTitle = () => _title;
this.getAuthor = () => _author;
this.getYear = () => _year;
this.updateYear = (newYear) => {
if (typeof newYear === 'number' && newYear > 0) {
_year = newYear;
console.log(`年份已更新为 ${newYear} 🎉`);
} else {
console.error("年份不合法!别乱来!🚫");
}
};
}
const book = new Book("JavaScript 高级程序设计", "Nicholas C. Zakas", 2011);
console.log(book.getTitle()); // ✅ 正常访问
// console.log(book._year); // ❌ 无法直接访问(虽然技术上可以,但不推荐)
book.updateYear(2025); // ✅ 安全更新
book.updateYear("泡面年"); // ❌ 被拦截
看!_title、_year 都是“私有”的,外部不能直接改,只能通过 updateYear 这种“安检通道”来修改。
🎉 这就是闭包的封装能力——像给变量穿上了“隐身衣”!
⚡ 第三章:防抖(Debounce)——别急,让我缓缓!
你有没有遇到过这种情况:
用户在搜索框疯狂打字:“j”→“ja”→“jav”→“java”→“javascript”……
每敲一个字,你就发一个请求?🤯
服务器:我谢谢你全家!
这时候,防抖(Debounce) 就该上场了!
🛑 防抖的核心思想:
“你尽管打字,等你停手 200ms 后,我再发请求。”
就像你妈催你吃饭:“你再玩 5 分钟手机就吃饭!”——但你每玩 1 分钟,她就重置一次倒计时。结果你玩了 30 分钟…… 😅
来,上代码:
function debounce(fn, delay) {
let timer = null; // 闭包变量:定时器 ID
return function (...args) {
clearTimeout(timer); // 清除之前的定时器
timer = setTimeout(() => {
fn.apply(this, args); // 执行函数
}, delay);
};
}
// 模拟搜索请求
function search(keyword) {
console.log(`🔍 搜索中:${keyword}`);
}
const debouncedSearch = debounce(search, 300);
// 模拟用户输入
debouncedSearch("j"); // 清除,重新计时
debouncedSearch("ja"); // 清除,重新计时
debouncedSearch("jav"); // 清除,重新计时
debouncedSearch("java"); // ✅ 300ms 后执行:搜索 "java"
🎯 防抖 = 频繁触发 → 只执行最后一次
应用场景:
- 搜索建议(Google Suggest)
- 窗口 resize
- 按钮防重复点击
🕹️ 第四章:节流(Throttle)——我得喘口气!
防抖是“等你停手”,节流是“我不管你怎么疯,我每隔 1s 才执行一次”。
就像游戏里的技能冷却:
“冲锋!” → 冷却 5s → “冲锋!” → 冷却 5s → ……
🚦 节流的核心思想:
“不管你按多快,我最多每 1s 响应一次。”
function throttle(fn, delay) {
let last = 0; // 上次执行时间
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
}
function reportPosition() {
console.log(` 当前位置:${Math.random().toFixed(4)}`);
}
const throttledReport = throttle(reportPosition, 1000);
// 模拟高频触发(比如 scroll 事件)
setInterval(throttledReport, 100); // 每 100ms 调用一次
// 但节流后,每 1000ms 才真正执行一次
输出:
当前位置:0.1234
当前位置:0.5678
当前位置:0.9012
🎯 节流 = 高频触发 → 固定频率执行
应用场景:
- 滚动事件(scroll)
- 鼠标移动(mousemove)
- 游戏技能冷却
💔 第五章:this 的“背叛”与闭包的“救赎”
JavaScript 中最让人头疼的,莫过于 this 的指向问题。
看这个经典例子:
const person = {
name: "小明",
sayHello: function () {
setTimeout(function () {
console.log(`${this.name} 说:你好!`); // ❌ this 指向 window!
}, 1000);
}
};
person.sayHello(); // 输出:undefined 说:你好! 😭
setTimeout 里的 this 是 window,不是 person!this 背叛了我们!💔
✅ 解法一:that = this
sayHello: function () {
const that = this; // 保存 this
setTimeout(function () {
console.log(`${that.name} 说:你好!`); // ✅
}, 1000);
}
✅ 解法二:箭头函数(推荐)
sayHello: function () {
setTimeout(() => {
console.log(`${this.name} 说:你好!`); // ✅ 箭头函数不绑定 this
}, 1000);
}
✅ 解法三:bind
sayHello: function () {
setTimeout(function () {
console.log(`${this.name} 说:你好!`);
}.bind(this), 1000); // ✅ 强制绑定
}
🎯 闭包在这里的作用:
that = this利用了闭包保存了this的引用!
🎭 第六章:IIFE —— 立即执行的“一次性烟花”
IIFE(Immediately Invoked Function Expression):立即执行函数表达式。
就像放烟花🧨:点火 → 爆炸 → 结束,全程不保留变量。
const Counter = (function () {
let count = 0; // 私有计数器
return function () {
return {
getCount: () => count,
increment: () => ++count,
reset: () => count = 0
};
};
})();
const c1 = Counter();
const c2 = Counter();
c1.increment(); // count = 1
c2.increment(); // count = 1(独立计数器!)
console.log(c1.getCount()); // 1
console.log(c2.getCount()); // 1
🎉 IIFE + 闭包 = 创建私有作用域 + 模块化
🧩 第七章:闭包的“超能力”大集合
| 能力 | 说明 | 示例 |
|---|---|---|
| 🛡️ 私有变量 | 外部无法直接访问 | let _private |
| 🧠 记忆函数 | 缓存计算结果 | 斐波那契记忆化 |
| ⏸️ 防抖 | 延迟执行,只执行最后一次 | 搜索框 |
| 🕹️ 节流 | 固定频率执行 | 滚动事件 |
| 🎭 柯里化 | 函数预配置 | add(1)(2) |
| 🔗 偏函数 | 固定部分参数 | partial(fn, arg1) |
| 🎪 事件监听 | 保存上下文 | addEventListener |
🚨 第八章:闭包的“副作用”——内存泄漏!
闭包虽好,但用不好会吃内存!
因为闭包会“留住”外部变量,导致它们无法被垃圾回收。
function heavyTask() {
const bigData = new Array(1000000).fill("💀"); // 100万条数据
return function () {
console.log("我还在用 bigData!"); // 闭包引用 bigData
};
}
const task = heavyTask(); // bigData 一直存在!
⚠️ 建议:
- 及时解除引用:
task = null- 避免在闭包中引用大型对象
- 使用 WeakMap / WeakSet 优化
🧠 总结:闭包,是“魔法”也是“责任”
闭包是 JavaScript 的灵魂特性之一。
它让你可以:
- 封装私有变量 ✅
- 实现防抖节流 ✅
- 保存上下文 ✅
- 构建模块系统 ✅
但也要小心:
- 内存泄漏 ❌
this指向问题 ❌- 调试困难 ❌
🎯 一句话总结:
闭包 = 函数 + 外部变量的引用 + 内存驻留
🙌 最后的话
闭包,就像一位隐身的守护者,默默保存着变量,控制着执行节奏,守护着代码的私密性。
当你第一次真正理解闭包时,那种“我悟了! ”的感觉,就像打通了任督二脉,瞬间从“前端小白”进化为“闭包大师”!