注册
web

彻底搞懂闭包

每次面试都问,每次都背;每次都背的不错,每次都不太理解。


定义


闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。


一个简单的例子认识闭包:


function init() {
var name = 'wendZzzoo';
function getName() {
console.log(name)
}
getName()
}
init()


使用场景


那闭包有什么作用呢?但从上面这个简单的例子中,似乎很难发现这样写,也就是闭包这样的写法的用途。


数据封装和隐藏


通过使用闭包,可以创建一个作用域限定的环境,以保护变量不受外部的访问和修改。这样可以防止变量被意外修改,避免命名冲突和全局污染,提高代码的可维护性和可读性。


举个例子,定义一个计数器函数,用来某些场景下计算次数。


没有使用闭包的示例


let count = 0
function increment() {
count++
console.log(count)
}
increment()
increment()


上述代码是实现了计数器的需求,但是代码存在风险,count变量是全局定义的,在后续开发中或者是其他人维护时可以轻易修改这个变量,导致bug出现。


使用闭包的示例


function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}

const counter = createCounter();
counter();
counter();


在上述示例中,createCounter函数返回了一个内层函数increment,该函数可以访问并递增count变量。外层函数的作用域被封装在闭包中,外部无法直接访问和修改count变量。


这里可以衍生思考一下,count变量封装在闭包中只能递增,外部无法修改,那该如何重置或者递减count呢?


其实需要新增的逻辑也可以封装到闭包里,以重置count为例:


const counterModule = (function() {
let count = 0;
function increment() {
return ++count;
}
function reset() {
count = 0;
}
return {
increment,
reset
};
})();

console.log(counterModule.increment());
console.log(counterModule.increment());
counterModule.reset();
console.log(counterModule.increment());


在上面的例子中,使用立即调用函数表达式(IIFE)创建了一个闭包,内部定义了count变量和两个操作它的函数incrementreset。通过返回一个包含这些函数的对象,实现了对count变量的封装和控制。


保持数据状态


通过闭包,内层函数可以访问和持有外层函数的变量,即使外层函数执行完毕,这些变量依然存在于内层函数的词法环境中,从而实现了数据状态的保持。


这个使用场景可以算是上一个的延申,在上述示例代码中添加传参,就可以起到了数据状态保持的目的。


函数柯里化


闭包使得函数可以返回另一个函数作为结果,从而形成函数工厂的模式。通过在内层函数中访问外层函数的参数或变量,可以创建具有不同参数或上下文的函数。这种技术称为柯里化,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。


举个例子,定义一个求矩形面积的函数。


不使用柯里化的示例


function getArea(width, height) {
return width * height
}
const area1 = getArea(10, 20)
console.log(area1)
const area2 = getArea(10, 30)
console.log(area2)
const area3 = getArea(10, 40)
console.log(area3)


上面代码里,假设我们需要这个计算矩形面积的函数,来计算宽度总是10的多种情况,那就需要多次调用getArea函数传入相同的宽度参数,且在维护的时候,假设需要统一修改宽度为20,就需要重复修改每一次调用时宽度的传参,这样重复的工作在力求优雅的情况下看来是不合适的。


使用闭包柯里化的示例


function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
const area1 = getTenWidthArea(20)
console.log(area1)
const area2 = getTenWidthArea(30)
console.log(area2)
const area3 = getTenWidthArea(40)
console.log(area3)


如果有需要宽度改变的情况,也可以轻松复用


const getTwentyWidthArea = getArea(20)


再举个例子,定义一个打印日志的函数。


function createLogger(prefix) {
function logger(log) {
console.log(`${prefix}: ${log}`);
}
return logger;
}

const exportWarnning = createLogger('warnning');
exportWarnning('这是一个警告日志');

const exportError = createLogger('error');
exportError('这是一个错误日志');


通过调用createLogger函数并传递不同的参数,可以创建具有不同日志前缀的logger函数。


通过上述两个柯里化的例子,可以发现柯里化是一种技术更多是一种提倡,使用这样的技术可以让你的代码更有维护性。


模拟私有化方法


私有方法是将某些函数或变量限定在一个作用域内,外部无法直接访问。


function makeCounter() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value());
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());
Counter1.decrement();
console.log(Counter1.value());
console.log(Counter2.value());


上述代码通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式


两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量


注意事项


闭包是一种强大的特性,但滥用闭包可能导致代码可读性和性能方面的问题,因此需要注意的是:



  1. 避免不必要的闭包,只有在确实需要保留状态或隐藏数据时才使用闭包。不要为了使用闭包而创建不必要的函数嵌套,盲目使用闭包并不会让你的代码看起来更高级。
  2. 要注意内存管理,闭包会持有对外部作用域的引用,可能导致内存泄漏。确保在不再需要闭包时,手动解除对外部作用域的引用,以便垃圾回收器能够正确处理。
  3. 特别要小心循环中的闭包,闭包会捕获循环变量的引用,可能导致意外结果。可以使用立即调用函数表达式(IIFE)或函数绑定来解决。
  4. 换一种解决方案,可以使用模块模式,如果需要封装私有方法和变量,考虑使用模块模式或其他模块化工具,如ES6模块。这样可以更清晰地定义私有和公共部分,并提供更好的可维护性和可测试性。

内存泄漏


闭包可以引起内存泄漏的情况,通常是涉及对外部作用域的引用。当函数形成闭包时,它会持有对其包含作用域的引用,这可能导致无法释放被闭包引用的内存。


可能导致内存泄漏的情况:



  1. 未及时释放闭包,如果闭包持有对外部作用域的引用,但不再需要使用闭包时,如果没有显式地解除对外部作用域的引用,闭包将继续存在并持有外部作用域中的变量。
  2. 当闭包和其包含作用域之间存在循环引用时,可能导致内存泄漏。例如,如果闭包中引用了一个对象,而该对象又持有对闭包的引用,这将导致它们互相引用,无法被垃圾回收。
  3. 闭包中引用了全局变量,闭包将一直存在,即使在不再需要闭包时也无法释放。这种情况下,全局变量将一直保持活动状态,无法被垃圾回收。

为避免闭包引起的内存泄漏,建议:



  1. 及时解除引用,当不再需要使用闭包时,确保手动解除对外部作用域的引用。只需要将闭包中引用的变量设置为 null 或重新分配其他值,以便垃圾回收器能够正确处理。
  2. 尽量避免闭包和其包含作用域之间的循环引用。确保在闭包中不引用外部对象,或者在外部对象中不引用闭包,以避免循环引用导致的内存泄漏。
  3. 只在确实需要保留状态或隐藏数据时使用闭包,在不需要闭包的情况下,使用适当的作用域(例如局部变量或模块作用域)来防止不必要的内存占用。

内存泄漏的发生并不一定是由闭包引起的,还可能涉及其他因素,但是,闭包在不正确使用的情况下容易导致内存泄漏问题。


作者:wendZzoo
来源:juejin.cn/post/7293805895918207026

0 个评论

要回复文章请先登录注册