闭包是 JavaScript 的核心难点之一,也是前端面试中的高频考点。要是没搞懂闭包,就很难真正吃透 JS 的作用域、内存管理,甚至函数式编程等核心概念。本文会用简单易懂的语言和一些典型的示例,帮你把闭包从原理到常见用法、易踩的坑都梳理一遍。
一、先搞清楚作用域和作用域链
理解闭包之前,先要把作用域和作用域链捋顺。
var n = 999;
function f1() {
// 没有用 var/let/const 声明,会直接挂到全局
b = 123;
{
// 块级作用域(ES6)
let a = 1;
}
console.log(n); // 999
}
f1();
console.log(b); // 123
n是全局变量,函数内部可以正常访问;a用let定义,只在块级作用域里有效;b没有使用声明关键字,执行后就成了全局变量(相当于window.b)。
二、闭包到底是什么?
📌 简单来说:
闭包就是函数和它引用的外部变量(自由变量)组合起来的一种结构。
只要一个函数在自己定义的作用域之外被调用,依然能访问它当初定义时的作用域里的变量,这就是闭包。
三、闭包经典示例:自由变量不会被销毁
示例 1:返回函数形成闭包
function f1() {
var n = 999; // 自由变量
function f2() {
console.log(n);
}
return f2;
}
var result = f1(); // f1 已执行完
result(); // 999
从执行顺序看,f1 已经跑完了,按理说局部变量 n 应该销毁了。但因为 f2 还在使用 n,所以它被一直保留在内存里。这就是闭包最直观的表现。
💡 这段示例就证明了:被内部函数引用的自由变量,不会被销毁,而是一直存在,直到引用消失。
示例 2:用闭包做数据封装
function f1() {
var n = 999;
// 提供修改接口
nAdd = function () {
n += 1;
}
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd(); // 修改 n
result(); // 1000
这里我们把 n 封装在函数作用域里,只能通过 nAdd 和 f2 来读写。这种写法经常被用来做“私有变量”。
✅ 同样可以看到:
n是f1的局部变量,但由于闭包存在,它不会被销毁,一直保留在内存里供后续使用。
四、this 与闭包:上下文易错点
闭包里用 this 很容易踩坑。下面这段代码就挺典型。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
},
};
var fn = object.getNameFunc();
console.log(fn()); // 输出:My Object
如果你直接写 return this.name,那么内部返回的函数单独调用时 this 会指向全局对象,结果输出 The Window 而不是 My Object。这里通过 var that = this 把当前上下文保存下来,再用闭包访问 that,就能保证拿到正确值。
💡 这里也能看到闭包的特性:
getNameFunc执行完后,that依然存在,因为被返回的函数引用了它。
五、闭包的几个典型用途
1. 读写函数内部变量(做私有化)
function counter() {
let count = 0;
return function () {
count++;
return count;
};
}
const c = counter();
c(); // 1
c(); // 2
像这种计数器,外面没法直接操作 count,只能通过闭包函数来访问,实现了简单的数据私有化。
2. 延迟执行、模块模式等
闭包也是很多高级用法的核心,比如:
- 给事件处理函数传递额外上下文
- 模块化封装(IIFE)
- 函数柯里化、高阶函数、缓存等
六、闭包的注意事项 ⚠️
1. 容易导致内存泄漏
function f1() {
var largeObj = new Array(1000000).fill('💾');
return function () {
console.log(largeObj[0]);
}
}
这里闭包函数引用了 largeObj,所以 largeObj 会常驻内存。如果这个变量不再需要,最好手动断开引用:
largeObj = null;
2. 修改外部变量带来的副作用
function outer() {
var count = 0;
return {
inc() { count++ },
log() { console.log(count) }
}
}
const c = outer();
c.inc();
c.log(); // 1
闭包可以修改父作用域的变量,这种可变性有时候会带来不可预期的结果。写的时候要留意状态的管理。
七、小结
| 特性 | 解释 |
|---|---|
| 访问外部变量 | 内部函数可以访问定义时父作用域的变量 |
| 延长变量生命周期 | 被引用的变量不会立即销毁,直到所有引用消失 |
| 实现封装 | 可以用来模拟私有变量 |
| 可能导致内存泄漏 | 若引用没释放,变量一直存在,需手动清理 |
✍️ 一句话总结闭包:
闭包是连接函数内部和外部变量的桥梁,让局部变量“自由”地活得更久。
📌 结尾
如果这篇文章对你有帮助,记得点赞、收藏或评论,后面我还会更新更多 JavaScript 进阶干货,咱们一块把 JS 基础打牢!