JavaScript 闭包:从入门到精通 🎯
用最简单的语言,带你彻底搞懂闭包这个"神秘"概念。
一、先讲个故事 📖
想象你有一个书包🎒。
你走出了教室,甚至走出了学校大门,但你的书包里还装着教室里老师发的试卷。
虽然你已经"离开"了教室,但你依然可以随时打开书包,看到那张试卷。
这,就是闭包的核心思想。
一个函数,即使离开了它被创建的地方,依然能"记住"并访问那个地方的变量。
二、在理解闭包之前,先搞懂"作用域" 🏠
什么是作用域?
作用域就像房间的墙壁。
🏠 大房子(全局作用域)
│
│ let name = "小明";
│
│ ┌─────────────────────┐
│ │ 🚪 小房间(函数作用域) │
│ │ │
│ │ let age = 14; │
│ │ │
│ │ // ✅ 在小房间里, │
│ │ // 既能看到 age, │
│ │ // 也能看到外面的 name │
│ └─────────────────────┘
│
│ // ❌ 在大房子里,
│ // 看不到小房间里的 age
规则很简单:
- 📌 里面的房间 → 可以看到外面的东西
- 📌 外面 → 看不到里面的东西
用代码来说就是:
javascript
let name = "小明";
function sayHello() {
let age = 14;
console.log(name); // ✅ 能访问外面的 name → "小明"
console.log(age); // ✅ 能访问自己的 age → 14
}
sayHello();
console.log(age); // ❌ 报错!外面看不到里面的 age
三、终于要讲闭包了! 🎉
最简单的闭包
javascript
function 妈妈() {
let 零花钱 = 100;
function 孩子() {
console.log("我有" + 零花钱 + "元零花钱");
}
return 孩子; // 妈妈把孩子"送出去"了
}
let 小明 = 妈妈(); // 妈妈函数已经执行完毕了
小明(); // "我有100元零花钱" ------ 居然还能访问"零花钱"!
等一下,这里发生了什么? 🤔
妈妈()函数执行完了,按理说里面的零花钱变量应该"消失"了- 但是!
孩子函数被 return 出来了,赋值给了小明 小明()执行时,依然记得零花钱 = 100
🎒 还记得开头的书包故事吗?
孩子函数就像那个书包,它"装着"对零花钱的记忆,走到哪里都忘不掉。
用正经的话重新说一遍
javascript
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
let fn = outer();
fn(); // 1
fn(); // 2
fn(); // 3
这就是闭包:
闭包 = 一个函数 + 它能访问的外部变量的环境
inner 函数 + 它记住的 count 变量 = 一个闭包。
四、闭包到底有什么用? 🔧
用途一:创建"私有变量" 🔒
在现实中,银行账户的余额不应该被随便修改,只能通过存钱、取钱操作。
javascript
function 创建银行账户(初始余额) {
let 余额 = 初始余额; // 外面谁也碰不到这个变量!
return {
查询余额() {
console.log(`当前余额:${余额}元`);
},
存钱(数额) {
if (数额 > 0) {
余额 += 数额;
console.log(`成功存入${数额}元`);
}
},
取钱(数额) {
if (数额 > 余额) {
console.log("余额不足!");
} else {
余额 -= 数额;
console.log(`成功取出${数额}元`);
}
}
};
}
let 我的账户 = 创建银行账户(500);
我的账户.查询余额(); // 当前余额:500元
我的账户.存钱(200); // 成功存入200元
我的账户.查询余额(); // 当前余额:700元
我的账户.取钱(1000); // 余额不足!
// 想直接改余额?没门!
console.log(我的账户.余额); // undefined ------ 根本访问不到 🔒
闭包让 余额 变成了"私有变量",只有内部的几个函数才能操作它,外界无法直接篡改。
用途二:函数工厂 🏭
闭包可以像工厂一样,批量制造功能类似但有差异的函数。
javascript
function 创建乘法器(倍数) {
return function(数字) {
return 数字 * 倍数;
};
}
let 双倍 = 创建乘法器(2);
let 三倍 = 创建乘法器(3);
let 十倍 = 创建乘法器(10);
console.log(双倍(5)); // 10
console.log(三倍(5)); // 15
console.log(十倍(5)); // 50
每次调用 创建乘法器,都会产生一个新的闭包 ,每个闭包"记住"了不同的 倍数。
用途三:计数器 🔢
javascript
function 创建计数器() {
let count = 0;
return {
加一() { count++; },
归零() { count = 0; },
获取() { return count; }
};
}
let 计数器A = 创建计数器();
let 计数器B = 创建计数器(); // 完全独立!
计数器A.加一();
计数器A.加一();
计数器A.加一();
console.log(计数器A.获取()); // 3
console.log(计数器B.获取()); // 0 ------ 互不影响!
五、经典面试题:循环中的闭包 🎯
这是无数人踩过的坑,看看你能不能答对。
❌ 问题代码
javascript
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
你觉得输出什么?
很多人以为是 0, 1, 2。
实际输出:3, 3, 3 😱
为什么?
用一个比喻来解释:
想象有 3 个同学,老师说"1秒后看黑板上的数字并大声念出来"。
但在这1秒内,老师一直在擦黑板重新写:先写0,擦掉写1,擦掉写2,擦掉写3。
1秒后,3个同学抬头一看 ------ 黑板上是 3。所以都念出了 3。
var 声明的 i 只有一个(全局共享的黑板),循环结束后 i 变成了 3,三个定时器读到的都是同一个 i。
✅ 用闭包修复
javascript
for (var i = 0; i < 3; i++) {
(function(j) { // 每次循环都创建一个"小房间"
setTimeout(function() {
console.log(j); // 每个小房间有自己的 j
}, 1000);
})(i); // 把当前的 i 传进去,"锁"在小房间里
}
// 输出:0, 1, 2 ✅
相当于每个同学拿到了一张自己的纸条📝,上面写着各自的数字,不管黑板怎么变,纸条上的数字不会变。
✅ 更现代的解决方案:用 let
javascript
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 输出:0, 1, 2 ✅
let 在每次循环时会自动创建一个新的变量副本(自带小房间),本质上和闭包方案一样。
六、进阶:闭包的高级应用 🚀
6.1 函数柯里化(Currying)
把一个接受多个参数的函数,变成一系列接受单个参数的函数。
javascript
// 普通写法
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// 柯里化写法(闭包链)
function curriedAdd(a) {
return function(b) { // 闭包,记住了 a
return function(c) { // 闭包,记住了 a 和 b
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
// 实际妙用:
let addFrom10 = curriedAdd(10);
let addFrom10And20 = addFrom10(20);
console.log(addFrom10And20(5)); // 35
console.log(addFrom10And20(30)); // 60
6.2 实现防抖(Debounce)
搜索框输入时,不希望每按一个键就发请求,而是等用户停下来后再发。
javascript
function debounce(fn, delay) {
let timer = null; // 闭包变量,记住定时器
return function(...args) {
clearTimeout(timer); // 每次调用,清除上一次的定时器
timer = setTimeout(() => { // 重新设一个定时器
fn.apply(this, args);
}, delay);
};
}
// 使用
let 智能搜索 = debounce(function(keyword) {
console.log("搜索:" + keyword);
}, 500);
// 模拟快速输入
智能搜索("J");
智能搜索("Ja");
智能搜索("Jav");
智能搜索("Java");
智能搜索("JavaScript");
// 只会输出最后一次:"搜索:JavaScript" ✅
timer 被闭包保护起来,外部无法干扰,但每次调用返回的函数都能访问并修改它。
6.3 实现缓存/记忆化(Memoization)
让函数"记住"之前的计算结果,避免重复计算。
javascript
function createMemo(fn) {
let cache = {}; // 闭包变量,缓存结果
return function(n) {
if (cache[n] !== undefined) {
console.log(`📦 从缓存读取 ${n} 的结果`);
return cache[n];
}
console.log(`🔨 计算 ${n} 的结果`);
let result = fn(n);
cache[n] = result; // 存入缓存
return result;
};
}
// 一个耗时的函数
function 斐波那契(n) {
if (n <= 1) return n;
return 斐波那契(n - 1) + 斐波那契(n - 2);
}
let 快速斐波那契 = createMemo(斐波那契);
快速斐波那契(30); // 🔨 计算 30 的结果(较慢)
快速斐波那契(30); // 📦 从缓存读取 30 的结果(瞬间)
七、闭包的"副作用":内存泄漏 ⚠️
闭包会让变量一直待在内存里不被回收。
javascript
function 创建大数据() {
let 巨大数组 = new Array(1000000).fill("🔥");
return function() {
console.log(巨大数组.length);
};
}
let fn = 创建大数据();
// 巨大数组 一直在内存中,因为 fn 还"记着"它
fn = null; // ✅ 手动解除引用,让垃圾回收器回收
比喻:闭包就像你一直不清理的书包 🎒,里面的东西越装越多。记得定期清理不需要的闭包引用!
避免内存泄漏的建议
javascript
// ✅ 不再需要时,将引用设为 null
let counter = 创建计数器();
// ...使用完毕后
counter = null;
// ✅ 只在闭包中引用必要的变量
function good() {
let needed = "我需要";
let huge = new Array(1000000); // 大对象
return function() {
// 只使用 needed,不使用 huge
// 但某些引擎可能仍保留 huge,取决于优化
console.log(needed);
};
}
八、一张图总结闭包 🗺️
┌─────────────────────────────────────────────┐
│ 闭包 (Closure) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 外部函数 │ │ 被记住的变量环境 │ │
│ │ │────▶│ │ │
│ │ function外() │ │ let x = 10 │ │
│ │ { │ │ let y = 20 │ │
│ │ return 内 │ │ │ │
│ │ } │ └───────┬──────────┘ │
│ └──────────────┘ │ │
│ │ 引用 │
│ ┌──────────────┐ │ │
│ │ 内部函数 │◀───────────┘ │
│ │ │ │
│ │ function内() │ 即使外部函数执行完毕, │
│ │ { │ 内部函数依然能访问 │
│ │ 用 x 和 y │ x 和 y │
│ │ } │ │
│ └──────────────┘ │
└─────────────────────────────────────────────┘
九、三句话记住闭包 💡
- 函数套函数,内部函数使用了外部函数的变量
- 内部函数被传到外面使用后,依然记得那些变量
- 那些变量不会被销毁,像被锁在保险箱里,只有这个内部函数有钥匙 🔑
十、最后的最后 🎬
闭包不是什么"高深莫测"的东西,你可能早就在不知不觉中用过了:
- 给按钮添加事件监听?闭包
- 用
setTimeout+ 外部变量?闭包 - React 的
useState?底层也是闭包
javascript
// 你可能天天在写闭包,只是不知道而已
document.getElementById("btn").addEventListener("click", function() {
let clickCount = 0; // 不对,这样每次都重置了
});
// 正确的闭包写法 ✅
let clickCount = 0;
document.getElementById("btn").addEventListener("click", function() {
clickCount++; // 这个回调函数 + 外部的 clickCount = 闭包!
console.log(`点击了 ${clickCount} 次`);
});
闭包就像呼吸一样自然 ------ 你一直在做,只是现在知道了它的名字。 😄
后记
2026年4月16日14点29分于上海,在opus 4.6辅助下完成。