1.关于闭包的概念
闭包是指一个函数能够访问并使用其父函数作用域中的变量,即使父函数已经执行完毕并返回了。闭包通常是由一个函数和对该函数的引用组成的。在 JavaScript 中,闭包是一种非常常见的编程模式,常用于实现模块化、私有变量、缓存、回调等功能。
闭包就像一把瑞士军刀,你以为你只是理解了区区八股文,但实际上你拥有了一个功能强大的工具,用的好可以解决各种棘手的问题,用的不好,可以让后来者来解决你制造的各种棘手的问题🤣🤣,并亲切的和你聊国粹的艺术。
2.简单示例
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
}
}
let increment = counter();
increment(); // 输出:1
increment(); // 输出:2
increment(); // 输出:3
increment = null; // 不使用闭包时,手动清除闭包
在上面的示例中,counter() 函数返回了一个匿名函数,该函数能够访问并使用 counter() 函数作用域中的 count 变量。当我们将 counter() 函数赋值给变量 increment 后,increment() 函数就是一个闭包了,每次调用 increment() 函数时,都会访问并更新 count 变量的值。
3.闭包常见的应用场景
1-实现模块化和私有变量
使用闭包可以实现模块化,将一些相关的函数和变量封装在一个函数中,并返回一个公共接口。这样就可以防止这些函数和变量被其他代码意外修改,提高了代码的安全性和可维护性。
在 JavaScript 中,没有原生的私有变量机制,但是可以通过闭包实现。将一个变量定义在函数内部,然后在函数返回的闭包中使用这个变量,这个变量就成为了私有变量,只能被闭包内的函数访问。
let counterModule = (function () {
let count = 0;
function increment() {
count++;
console.log('Count:', count);
}
function decrement() {
count--;
console.log('Count:', count);
}
return {
increment,
decrement
};
})();
counterModule.increment(); // 输出:Count: 1
counterModule.increment(); // 输出:Count: 2
counterModule.decrement(); // 输出:Count: 1
counterModule = null; // 不使用闭包时,手动清除
在上面的示例中,我们使用了立即执行函数(IIFE)来创建一个闭包,并且在闭包内部定义了两个函数 increment() 和 decrement(),用于对一个私有变量 count 进行增加和减少操作。然后我们返回一个包含这两个函数的对象,这样就可以通过该对象对外提供这些方法,从而实现模块化。
通过使用闭包,count 变量被定义在 createCounter() 函数内部,外部代码无法直接访问它,从而实现了私有变量的效果。我们可以将 count 变量隐藏在闭包内部,防止外部代码对其进行修改。这样可以有效地避免命名冲突和数据污染等问题,同时也可以提高代码的可维护性和安全性。
2-缓存数据
闭包可以用于实现数据缓存,当需要多次计算某个值时,可以将计算结果缓存起来,下次需要时直接使用缓存中的值,避免重复计算。
function cache(func) {
const cached = {};
return function(arg) {
if (cached[arg]) {
console.log('Using cached data for ' + arg);
return cached[arg];
} else {
console.log('Calculating result for ' + arg);
const result = func(arg);
cached[arg] = result;
return result;
}
}
}
function expensiveCalculation(num) {
let result = 0;
for (let i = 1; i <= num; i++) {
result += i;
}
return result;
}
let cachedCalculation = cache(expensiveCalculation);
console.log(cachedCalculation(10)); // 输出:Calculating result for 10 55
console.log(cachedCalculation(10)); // 输出:Using cached data for 10 55
console.log(cachedCalculation(20)); // 输出:Calculating result for 20 210
console.log(cachedCalculation(20)); // 输出:Using cached data for 20 210
cachedCalculation = null; //不使用闭包时,手动清除
在上面的示例中,我们定义了一个 cache() 函数,该函数接受一个参数 func,代表需要缓存的函数。在 cache() 函数内部,我们定义了一个 cached 对象,用于存储缓存数据。然后返回一个闭包函数,用于实际调用缓存的数据。
在闭包函数内部,我们首先判断 cached 对象中是否已经存在缓存数据。如果存在,就直接返回缓存数据,否则就调用传入的 func 函数进行计算,并将计算结果存储在 cached 对象中,最后返回计算结果。
通过这种方式,我们可以避免多次重复计算相同的值,从而提高代码的性能和效率。
3-实现回调函数
在 JavaScript 中,回调函数是一种常见的编程模式。使用闭包可以实现一些有趣的回调函数,例如事件监听、异步调用等。闭包可以让回调函数访问其所在函数的变量,从而实现更加灵活的编程。
function add(a, b, callback) {
const result = a + b;
callback(result);
}
function printResult(result) {
console.log('The result is: ' + result);
}
add(1, 2, printResult); // 输出:The result is: 3
在上面的示例中,我们定义了一个 add() 函数,该函数接受三个参数:a 和 b 分别代表两个加数,callback 代表回调函数。在函数内部,我们先计算出两个加数的和 result,然后将其作为参数传递给回调函数 callback。
然后我们又定义了一个 printResult() 函数,用于打印计算结果。在调用 add() 函数时,我们将 printResult 函数作为回调函数传递给 add() 函数,当计算完成后,add() 函数会调用该回调函数并传递计算结果作为参数,然后我们就可以在回调函数内部处理计算结果。
通过这种方式,我们可以在异步操作完成后,执行一些额外的操作,从而实现更加灵活的编程方式。
4.闭包的缺点和解决办法
闭包的缺点主要有两个:
- 内存泄漏:由于闭包中的函数引用了外部函数的变量,而外部函数的作用域在函数执行结束后并不会被销毁,这就导致了闭包函数中的变量也无法被销毁,从而占用了内存空间。如果闭包被滥用,可能会导致内存泄漏的问题。
- 性能问题:闭包中的函数访问外部函数的变量需要通过作用域链来查找,而作用域链的长度决定了查找的速度。如果闭包层数较深,作用域链就会很长,从而影响了函数的执行效率。
为了解决闭包的这些问题,可以采取以下一些措施:
- 及时释放闭包:如果不再需要使用闭包,可以手动将其赋值为 null,从而释放闭包中占用的内存空间。
- 减少闭包层数:尽量减少闭包层数,避免作用域链过长,从而提高函数的执行效率。
- 使用立即执行函数:可以使用立即执行函数来避免闭包的内存泄漏问题。由于立即执行函数在执行结束后会被立即销毁,因此其中的变量也会被释放。
- 使用模块化编程:可以使用模块化编程来避免闭包的性能问题。在模块化编程中,每个模块都是一个独立的作用域,不会对全局作用域造成影响,从而避免了作用域链过长的问题。
function foo() {
let count = 0;
return function() {
return ++count;
};
}
let counter = foo(); // 创建闭包
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
counter = null; // 释放闭包