一、什么是闭包?
闭包 = 函数 + 它能访问的外部变量
换句话说:
闭包就是一个函数能够"记住"并访问它定义时所在的环境中的变量。
听起来有点抽象?没关系,我们先从一个简单的例子开始。
示例 1:一个最简单的闭包
js
function outer() {
const name = "小明";
function inner() {
console.log(name); // 使用了外部函数 outer 的变量 name
}
return inner;
}
const sayName = outer(); // 这里返回的是 inner 函数本身
sayName(); // 输出 "小明"
分析这个过程:
outer()
函数执行后返回了内部的inner
函数。- 外部变量
name
是在outer()
内部定义的。 - 即使
outer()
执行完了,它的局部变量name
并没有被销毁。 sayName()
实际上是inner()
函数,它仍然能访问到name
变量。
这就是闭包的核心特性:即使外层函数执行完毕,其内部变量依然可以被内部函数访问和保留。
二、闭包的本质:函数+作用域链
闭包不是一种语法结构,而是一种行为模式。它的本质是:
当一个函数能够访问并记住其词法作用域(也就是它定义时所处的作用域),即使该函数在其作用域外执行,也形成了闭包。
示例 2:闭包让变量"私有化"
js
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const increment = counter();
increment(); // 输出 1
increment(); // 输出 2
increment(); // 输出 3
解释:
counter()
返回了一个函数,这个函数每次调用都会增加count
。count
是counter()
中的局部变量,正常情况下函数执行完后应该被销毁。- 但由于返回的函数"记住了"这个变量,所以它不会被销毁,而是继续存在内存中。
- 我们通过闭包实现了对
count
的保护和控制 ------ 这就是 JavaScript 中实现"私有变量"的一种方式!
三、闭包的常见应用场景
闭包非常强大,它在很多地方都有广泛的应用,下面是一些最常见的用途:
数据封装(私有变量)
就像上面的例子一样,我们可以创建一些只能通过特定方法访问的变量。
js
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
},
getCount: function () {
return count;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出 1
// count 无法直接访问或修改
优点:
- 避免全局变量污染
- 提高数据安全性
柯里化(Currying)与函数工厂
闭包可以用来生成定制化的函数。
js
function add(x) {
return function (y) {
return x + y;
};
}
const add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(10)); // 输出 15
这里 add(5)
返回了一个新函数,它"记住"了 x=5
,这就是闭包的力量。
回调函数中保存状态
闭包常用于事件处理、异步编程中保存上下文。
js
function setupTimer(name) {
setTimeout(function () {
console.log("你好," + name);
}, 1000);
}
setupTimer("张三"); // 1秒后输出 "你好,张三"
在这个例子中,匿名回调函数访问了 setupTimer
中的 name
,这其实也是闭包。
循环中绑定事件(经典问题)
这是初学者最容易踩坑的地方:
html
<ul>
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
js
const items = document.querySelectorAll("li");
for (var i = 0; i < items.length; i++) {
items[i].addEventListener("click", function () {
alert("你点击了第 " + i + " 个项目");
});
}
你会发现无论点击哪个 <li>
,弹出的都是"第 3 个项目"。
为什么会这样?
因为 var
是函数作用域,循环结束后 i
的值已经是 3。所有回调函数共享同一个 i
。
使用闭包解决这个问题:
js
for (var i = 0; i < items.length; i++) {
(function (index) {
items[i].addEventListener("click", function () {
alert("你点击了第 " + index + " 个项目");
});
})(i);
}
或者更简单的方式(推荐):
js
for (let i = 0; i < items.length; i++) {
items[i].addEventListener("click", function () {
alert("你点击了第 " + i + " 个项目");
});
}
let
是块级作用域,每个循环都会创建一个新的 i
,相当于自动形成了闭包。
四、闭包的缺点
虽然闭包很强大,但也有一些需要注意的问题:
缺点 | 说明 |
---|---|
内存占用高 | 闭包会保留外部函数的变量在内存中,可能导致内存泄漏 |
性能影响 | 如果滥用闭包,可能会影响程序性能 |
调试困难 | 闭包嵌套多层时,调试起来可能比较复杂 |
五、一句话总结闭包
闭包是一个函数能够访问并记住它定义时所处的作用域中的变量,即使该函数在该作用域之外执行。
也可以简化为:
闭包 = 函数 + 它能访问的外部变量
六、闭包练习题(动手试试看)
练习1:
js
function outer() {
var x = 10;
return function inner(y) {
return x + y;
};
}
var result = outer();
console.log(result(5)); // 输出多少?
练习2:
js
function createMultiplier(multiplier) {
return function (num) {
return num * multiplier;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 输出多少?
练习3(经典面试题):
js
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
这段代码会输出什么?如何修改让它依次输出 1、2、3?
七、闭包 vs 其他概念对比
概念 | 特点 |
---|---|
闭包 | 函数 + 外部变量,保持变量不被销毁 |
作用域 | 控制变量的可访问范围 |
this | 动态绑定,取决于函数调用方式 |
箭头函数 | 没有自己的 this,继承外层 this |
模块模式 | 利用闭包实现模块封装、暴露接口 |
八、结语
闭包是 JavaScript 中非常核心、也非常强大的概念。虽然刚开始可能会觉得有点绕,但只要你理解了它的原理和使用场景,就能写出更优雅、更高效的代码。
闭包就像是你的函数随身带着一个小背包,里面装着它出生时周围的变量。
如果你现在看完这篇文章,已经能回答这些问题:
- 什么是闭包?
- 为什么需要闭包?
- 闭包有什么好处和坏处?
- 常见的闭包写法有哪些?
- 如何避免闭包带来的问题?
那么恭喜你,你已经掌握了 JavaScript 中最重要的概念之一!