写出一个简单的JavaScript闭包示例,并解释为什么它形成了闭包。
写出一个简单的JavaScript闭包示例,并解释为什么它形成了闭包。
闭包示例代码
function outerFunction() {
// 定义一个变量,该变量处于 outerFunction 的作用域内
let message = '这是一个闭包示例';
// 定义一个内部函数 innerFunction
function innerFunction() {
// 内部函数可以访问外部函数作用域中的变量 message
console.log(message);
}
// 返回内部函数
return innerFunction;
}
// 调用 outerFunction 并将返回的 innerFunction 赋值给变量 closure
let closure = outerFunction();
// 调用 closure,实际上就是调用 innerFunction
closure();
代码解释(大白话版)
代码执行过程
- 定义外部函数
outerFunction
:就好像我们建了一个大房子(外部函数的作用域),在这个大房子里我们放了一个物品message
(定义了一个变量message
)。 - 定义内部函数
innerFunction
:在这个大房子里面又隔出了一个小房间(内部函数的作用域),小房间里的人(内部函数)可以看到大房子里的物品message
,并且可以使用它。 - 返回内部函数:我们把小房间的钥匙(内部函数
innerFunction
)交出去了,虽然大房子的门关上了(外部函数执行完毕),但是拿到钥匙的人(变量closure
)仍然可以打开小房间,并且通过小房间看到大房子里的物品message
。 - 调用
closure
:拿到钥匙的人打开小房间,看到了大房子里的物品message
,并把它打印了出来。
为什么形成了闭包
闭包形成的原因是内部函数 innerFunction
访问了它外部函数 outerFunction
作用域中的变量 message
。当外部函数 outerFunction
执行完毕后,按照正常情况,它里面的变量 message
应该被销毁,因为它的作用域已经结束了。但是由于内部函数 innerFunction
引用了这个变量,所以 JavaScript 引擎不会销毁 message
,而是会保留它,让内部函数在需要的时候仍然可以访问。这就形成了一个闭包,简单来说,闭包就是一个函数能够记住并访问它外部函数作用域中的变量,即使外部函数已经执行完毕。
在这个例子中,closure
变量保存了 innerFunction
,当我们调用 closure()
时,innerFunction
仍然可以访问 outerFunction
作用域中的 message
变量,这就是闭包的体现。
JavaScript 中闭包的优缺点
优点
1. 数据保密和封装
闭包可以把一些变量藏起来,就像你有一个带锁的箱子,只有特定的人(函数)才能打开它拿里面的东西,这样可以避免外部随意访问和修改这些数据,起到保护数据的作用。
代码示例:
function createBankAccount() {
// 这是藏在箱子里的钱,外部不能直接拿到
let balance = 0;
return {
// 存钱的操作
deposit: function(amount) {
balance = balance + amount;
return balance;
},
// 取钱的操作
withdraw: function(amount) {
if (amount <= balance) {
balance = balance - amount;
return balance;
}
return '余额不足';
},
// 查看余额的操作
getBalance: function() {
return balance;
}
};
}
// 创建一个银行账户
let myAccount = createBankAccount();
// 存钱
console.log(myAccount.deposit(100));
// 取钱
console.log(myAccount.withdraw(50));
// 查看余额
console.log(myAccount.getBalance());
解释:这里的 balance
变量就像是被锁在 createBankAccount
这个“箱子”里,外部不能直接访问它。只能通过 deposit
、withdraw
和 getBalance
这些“钥匙”(函数)来对它进行操作,保证了数据的安全。
2. 让变量一直“活着”
闭包能让外部函数里的变量一直存在于内存中,不会因为外部函数执行完就消失。就好像你在一个地方放了个东西,即使你离开了那个地方,东西还在那里,下次回来还能接着用。
代码示例:
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
// 创建一个计数器
let counter = makeCounter();
// 第一次调用计数器
console.log(counter());
// 第二次调用计数器
console.log(counter());
解释:count
变量在 makeCounter
函数里,当 makeCounter
执行完返回内部函数后,count
并没有消失。每次调用 counter
函数时,count
的值都会继续增加,因为闭包让它一直存在于内存中。
3. 灵活使用函数
闭包可以让我们把函数变得更灵活,就像搭积木一样,可以根据不同的需求组合出不同的功能。
代码示例:
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
// 创建一个乘以 2 的函数
let double = multiplier(2);
// 创建一个乘以 3 的函数
let triple = multiplier(3);
// 使用乘以 2 的函数
console.log(double(5));
// 使用乘以 3 的函数
console.log(triple(5));
解释:multiplier
函数根据传入的 factor
参数返回一个新的函数。我们可以用这个函数创建出不同的乘法函数,比如乘以 2 的 double
函数和乘以 3 的 triple
函数,非常灵活。
缺点
1. 占内存
闭包会让一些变量一直待在内存里不被清理,就像你家里有一些不用的东西一直堆着占地方。如果闭包用得太多或者用得不好,会让内存占用越来越大,可能导致电脑或者程序变慢。
代码示例:
function createBigClosure() {
// 这里创建了一个很大的数组,占很多内存
let bigArray = new Array(1000000).fill(0);
return function() {
return bigArray.length;
};
}
let closure = createBigClosure();
// 即使 createBigClosure 执行完了,bigArray 还在内存里占着地方
解释:bigArray
是一个很大的数组,由于闭包引用了它,即使 createBigClosure
函数执行完了,bigArray
也不会被清理掉,一直占用着内存。
2. 运行速度慢
使用闭包时,程序查找变量会比较麻烦,就像你在一个很大的仓库里找东西,要一层一层地找,所以会比直接找东西慢一些。
代码示例:
function outer() {
let outerVar = 10;
function inner() {
return outerVar;
}
return inner;
}
let innerFunc = outer();
// 调用 innerFunc 时,要去闭包的作用域里找 outerVar,比较慢
console.log(innerFunc());
解释:当调用 innerFunc
时,程序需要在闭包的作用域里找 outerVar
,这比直接在当前作用域里找变量要慢一些,因为要多走一些“路”。
3. 代码难懂难维护
闭包会让代码变得复杂,就像一团乱麻,很难理清里面的关系。特别是闭包嵌套很多层的时候,别人看你的代码或者你自己过段时间再看,可能都不知道代码是怎么运行的。
代码示例:
function outer() {
let outerVar = 1;
return function middle() {
let middleVar = 2;
return function inner() {
return outerVar + middleVar;
};
};
}
let innerFunc = outer()();
console.log(innerFunc());
解释:这里有三层闭包,要理解 inner
函数怎么拿到 outerVar
和 middleVar
需要费点脑筋,这就增加了代码的理解和维护难度。
闭包在实际开发中用途广泛,下面用大白话详细介绍几种常见的应用场景。
1. 实现私有变量和方法
想象一下你有一个小房间,里面放着一些重要的东西,你不想让别人随意进出这个房间拿走或改变里面的东西。在编程里,闭包就可以帮你实现这样的“小房间”。
比如你要创建一个人的对象,这个人有个名字,但你不想让别人直接修改这个名字,只能通过特定的方式来查看和修改。
function createPerson() {
// 这个名字就像放在小房间里的东西
let name = '张三';
return {
// 这个方法就像是一把钥匙,能让你查看小房间里的名字
getName: function() {
return name;
},
// 这个方法也是一把钥匙,能让你修改小房间里的名字
setName: function(newName) {
name = newName;
}
};
}
let person = createPerson();
console.log(person.getName());
person.setName('李四');
console.log(person.getName());
在这个例子中,name
变量被闭包保护起来,外部没办法直接访问它,只能通过 getName
和 setName
方法来操作,就像只有拿着特定钥匙的人才能进出小房间一样。
2. 函数柯里化
函数柯里化就像是搭积木,你可以把一个大的积木(多参数函数)拆分成一个个小积木(单参数函数),然后根据需要一块一块地组合起来。
比如说有一个加法函数,它本来要同时接收两个数字才能算出它们的和。但通过闭包,你可以让它先接收一个数字,然后返回一个新的函数,这个新函数再接收另一个数字,最后算出和。
function add(a, b) {
return a + b;
}
// 这个函数就像一个积木拆分器
function curry(func) {
return function(a) {
return function(b) {
return func(a, b);
};
};
}
let curriedAdd = curry(add);
console.log(curriedAdd(3)(5));
这里的 curriedAdd
函数就像一个可以分步使用的积木,先给它一个数字 3,它返回一个新的函数,再给这个新函数一个数字 5,就得到了最终的结果。
3. 事件处理
在网页开发中,经常会有一些按钮、链接之类的元素,当你点击它们时会触发一些操作。闭包可以帮助这些操作记住一些状态信息。
想象一下有一个按钮,你每点击一次,它就会记录你点击的次数。
<!DOCTYPE html>
<html lang="en">
<body>
<button id="myButton">点击我</button>
<script>
let count = 0;
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
count++;
console.log(`你点击了 ${count} 次`);
});
</script>
</body>
</html>
在这个例子中,当你点击按钮时,事件处理函数就像一个小助手,它能记住外部的 count
变量,每次点击就把这个变量的值加 1,然后告诉你点击了多少次。
4. 实现循环中的延迟操作
有时候你需要在循环里设置一些延迟执行的操作,就像你要给几个朋友依次发消息,但每个消息都要隔一段时间再发。闭包可以保证每个延迟操作都能拿到正确的循环变量值。
for (let i = 0; i < 5; i++) {
(function(index) {
setTimeout(function() {
console.log(`这是第 ${index} 次延迟输出`);
}, index * 1000);
})(i);
}
这里用一个小技巧(立即执行函数)创建了闭包,把当前的 i
值传给内部的函数。这样每个 setTimeout
函数就像一个小信使,能准确地记住自己是第几次循环,然后按顺序依次输出信息。
5. 模块化开发
在开发一个大项目时,就像建一座大房子,你会把它分成很多小房间(模块),每个小房间有自己的东西(变量和方法),而且要避免不同房间的东西混在一起。闭包可以帮你实现这样的模块化。
const myModule = (function() {
// 这个变量就像小房间里的一个宝贝
let privateVariable = 10;
// 这个方法就像小房间里的一个工具
function privateMethod() {
return privateVariable * 2;
}
return {
// 这个方法就像小房间的一扇窗户,外面的人可以通过它使用里面的工具
publicMethod: function() {
return privateMethod();
}
};
})();
console.log(myModule.publicMethod());
在这个例子中,通过闭包创建了一个独立的模块 myModule
。模块内部有自己的私有变量和方法,外部只能通过 publicMethod
这个“窗户”来间接使用里面的东西,这样就避免了和其他代码产生冲突。