各位靓仔靓女,晚上好!我是今晚的主讲人,咱们今天聊聊JavaScript里一个既让人爱又让人恨的家伙——闭包(Closure)。说它让人爱,是因为它强大到可以实现很多高级技巧;说它让人恨,是因为一不小心就掉进内存泄漏的坑里,性能嗖嗖地往下掉。
咱们今天就来扒一扒闭包的底裤,看看它的内存占用和性能陷阱,以及如何优雅地避开它们。准备好了吗?Let’s go!
一、什么是闭包?(通俗易懂版)
想象一下,你有一个秘密小盒子,里面装着一些宝贝(变量)。你把这个盒子锁起来,然后把盒子外面再包一层,做成一个更大的盒子。外面的盒子可以被别人拿到,但是外面的盒子没办法直接打开里面的小盒子。只有当初制造这个盒子的人,才有一把特殊的钥匙,能打开小盒子,拿到里面的宝贝。
这个“小盒子”就是闭包,它能记住自己出生时候的环境(变量),即使这个环境已经消失了,它还是能访问到这些变量。
用代码来说:
function outerFunction(outerVar) {
function innerFunction(innerVar) {
console.log("outerVar:", outerVar);
console.log("innerVar:", innerVar);
}
return innerFunction;
}
const myClosure = outerFunction("Hello"); // outerFunction执行完毕,但innerFunction仍然存在
myClosure("World"); // 输出 "outerVar: Hello" 和 "innerVar: World"
在这个例子中,innerFunction
就是一个闭包。即使outerFunction
已经执行完毕,innerFunction
仍然可以访问到outerVar
变量。 这就是闭包的核心特性:记住并访问其词法作用域中的变量,即使在其词法作用域之外执行。
二、闭包的内存占用:谁偷了我的内存?
闭包很酷,但它也是个内存小偷!因为闭包会持有对外部函数变量的引用,这些变量就不会被垃圾回收器回收,从而导致内存占用增加。
- 长期持有外部变量:
如果一个闭包长期存在,它所引用的外部变量也会长期存在于内存中。这就像你把那个秘密小盒子一直锁在抽屉里,里面的宝贝就一直占着空间。
function createCounter() {
let count = 0; // 这个变量会被闭包长期持有
return function() {
count++;
console.log(count);
}
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// 即使createCounter执行完毕,count变量仍然存在于内存中
在这个例子中,count
变量会被闭包长期持有,直到counter
变量被销毁。
- 循环中的闭包:
这是闭包内存泄漏的重灾区!如果在循环中创建闭包,并且闭包引用了循环变量,那么很容易导致大量的变量被闭包持有,造成内存泄漏。
function createFunctions() {
const functions = [];
for (var i = 0; i < 5; i++) { // 注意这里使用var,而不是let
functions.push(function() {
console.log(i); // 闭包引用了循环变量i
});
}
return functions;
}
const functionList = createFunctions();
functionList[0](); // 5
functionList[1](); // 5
functionList[2](); // 5
functionList[3](); // 5
functionList[4](); // 5
为什么会输出5个5? 因为在for
循环中使用var
声明的i
变量是全局作用域的,当循环结束时,i
的值变成了5。所有的闭包都引用的是同一个i
变量,所以输出的都是5。更糟糕的是,这5个闭包都持有了对全局变量i
的引用,直到脚本结束,i
才会被回收。
解决方案:
-
使用
let
或const
: 使用let
或const
声明循环变量,每次循环都会创建一个新的变量副本,闭包引用的就是这个副本。function createFunctionsWithLet() { const functions = []; for (let i = 0; i < 5; i++) { // 使用let functions.push(function() { console.log(i); }); } return functions; } const functionListWithLet = createFunctionsWithLet(); functionListWithLet[0](); // 0 functionListWithLet[1](); // 1 functionListWithLet[2](); // 2 functionListWithLet[3](); // 3 functionListWithLet[4](); // 4
-
使用IIFE(立即执行函数): 使用IIFE可以创建一个新的作用域,将循环变量作为参数传递给IIFE,闭包引用的是IIFE中的变量。
function createFunctionsWithIIFE() { const functions = []; for (var i = 0; i < 5; i++) { (function(j) { // 使用IIFE functions.push(function() { console.log(j); }); })(i); // 将i作为参数传递给IIFE } return functions; } const functionListWithIIFE = createFunctionsWithIIFE(); functionListWithIIFE[0](); // 0 functionListWithIIFE[1](); // 1 functionListWithIIFE[2](); // 2 functionListWithIIFE[3](); // 3 functionListWithIIFE[4](); // 4
- DOM元素引用:
如果闭包引用了DOM元素,并且DOM元素被移除,但闭包仍然存在,那么DOM元素占用的内存就无法被回收,导致内存泄漏。
function createButton() {
const button = document.createElement('button');
button.textContent = 'Click me';
document.body.appendChild(button);
let count = 0;
button.addEventListener('click', function() {
count++;
console.log('Clicked:', count);
});
return button;
}
const myButton = createButton();
// 假设稍后将myButton从DOM中移除:
// document.body.removeChild(myButton);
// 如果myButton变量仍然存在,那么button元素及其事件监听器占用的内存仍然无法被回收。
// 因为addEventListener创建的闭包持有对button的引用
解决方案:
-
手动解除引用: 在DOM元素被移除后,手动解除闭包对DOM元素的引用。
// 移除事件监听器,解除闭包对DOM元素的引用 myButton.removeEventListener('click', myButton.onclick); // 假设之前的事件监听器是内联的 // 或者使用addEventListener时保存listener函数,在移除时使用 document.body.removeChild(myButton); myButton = null; // 将变量设置为null,帮助垃圾回收器回收内存
三、闭包的性能陷阱:慢如蜗牛?
除了内存占用,闭包还可能影响性能。
- 变量查找时间:
闭包会增加变量查找的时间。当访问闭包中的变量时,JavaScript引擎需要沿着作用域链向上查找,直到找到该变量为止。如果闭包嵌套很深,那么变量查找的时间就会更长。
function outerFunction() {
let outerVar = "Outer";
function middleFunction() {
let middleVar = "Middle";
function innerFunction() {
console.log(outerVar, middleVar); // 需要沿着作用域链查找outerVar和middleVar
}
return innerFunction;
}
return middleFunction;
}
const myFunc = outerFunction()();
myFunc(); // 输出 "Outer Middle"
在这个例子中,innerFunction
需要沿着作用域链向上查找outerVar
和middleVar
,这会增加变量查找的时间。虽然对于现代JavaScript引擎来说,这点开销通常可以忽略不计,但是在对性能要求非常高的场景下,还是需要注意。
- 创建大量闭包:
如果创建大量的闭包,会占用大量的内存,并且增加垃圾回收器的负担,从而影响性能。
function createClosures(count) {
const closures = [];
for (let i = 0; i < count; i++) {
closures.push(function() {
console.log(i);
});
}
return closures;
}
const manyClosures = createClosures(10000); // 创建10000个闭包
// 每次调用一个闭包,都会增加垃圾回收器的负担
解决方案:
- 避免不必要的闭包: 尽量避免创建不必要的闭包。如果不需要访问外部变量,就不要使用闭包。
- 使用函数表达式代替函数声明: 函数表达式可以避免函数提升,从而减少闭包的创建。
- 使用bind()方法:
bind()
方法可以创建一个新的函数,并将this
绑定到指定的对象。bind()
方法也可以用来创建闭包,但是需要谨慎使用,避免过度使用。
四、闭包的常见应用场景:妙用无穷!
虽然闭包有一些缺点,但它也是一个非常强大的工具,可以用来实现很多高级技巧。
- 封装变量:
闭包可以用来封装变量,防止外部访问和修改。这就像把宝贝锁在秘密小盒子里,只有你知道钥匙。
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
} else {
return "Insufficient balance";
}
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(100);
console.log(myAccount.deposit(50)); // 150
console.log(myAccount.withdraw(20)); // 130
console.log(myAccount.getBalance()); // 130
// myAccount.balance = 0; // 无法直接访问和修改balance变量
在这个例子中,balance
变量是私有的,只能通过deposit
、withdraw
和getBalance
方法访问和修改。
- 创建模块:
闭包可以用来创建模块,将相关的函数和变量组织在一起。
const myModule = (function() {
let privateVar = "Secret";
function privateFunction() {
console.log("I am a private function");
}
return {
publicVar: "Public",
publicFunction: function() {
console.log("I am a public function");
privateFunction(); // 可以在公共函数中访问私有函数
console.log(privateVar); // 可以在公共函数中访问私有变量
}
};
})();
console.log(myModule.publicVar); // "Public"
myModule.publicFunction(); // "I am a public function" 和 "I am a private function" 和 "Secret"
// console.log(myModule.privateVar); // 无法访问私有变量
// myModule.privateFunction(); // 无法调用私有函数
在这个例子中,privateVar
和privateFunction
是私有的,只能在模块内部访问。
- 实现柯里化和偏函数:
闭包可以用来实现柯里化和偏函数,将函数分解成更小的、更灵活的函数。
-
柯里化: 将一个接受多个参数的函数转换成一系列接受单个参数的函数。
function add(x, y) { return x + y; } function curryAdd(x) { return function(y) { return x + y; } } const add5 = curryAdd(5); console.log(add5(3)); // 8 console.log(add5(10)); // 15
-
偏函数: 固定函数的部分参数,创建一个新的函数。
function log(level, message) { console.log(`[${level}] ${message}`); } function logError(message) { return log("ERROR", message); } logError("Something went wrong"); // [ERROR] Something went wrong
五、总结:爱恨交织的闭包
闭包是一个强大的工具,但也是一个潜在的陷阱。在使用闭包时,需要注意以下几点:
- 避免不必要的闭包。
- 注意闭包的内存占用,避免内存泄漏。
- 注意闭包的性能影响,避免过度使用。
总之,要像对待心爱的宠物一样,好好呵护你的闭包,才能让它为你所用,而不是反过来拖累你。
希望今天的分享对大家有所帮助!下次再见!