昨儿个,我们知晓了闭包,就是那个有着“虽已离开家庭,却仍记得家中密码”这般奇妙特性的函数。今儿咱们要深入探究一番:闭包此等事物究竟能够达成怎样的功用?是否存在副作用?又该如何避免它将内存耗尽?读完这篇内容,你不但会明白闭包的应用方式,还能够在面试场景中面对面试官时侃侃而谈。前言。
闭包如同那样一个租客,好似“赖着不走”一般。你原本觉得这人已经离开了,然而实际上他依旧留存着你的钥匙,还时不时回来取用一些物品。这种情况在JavaScript当中,有时会显得格外好用,可有时又会特别坑人。
咱们今儿就来清点一下闭包的若干经典运用场景,顺带说一说怎样能促使它“得体地退场”,莫要使汝的内存被吃尽。
一、闭包的应用场景,这个有着“赖着不走”特性的家伙居然蛮有用处,它可用于模块化,形成私有变量以及公共方法。
JavaScript在没有ES6模块的时候,闭包可是实现模块化的主要方式呢。它能够将内部的细节给隐藏起来,仅仅去暴露那些需要公开的接口呀。
const counter = (function() {
let count = 0; // 私有变量,外面访问不到
function increment() {
count++;
console.log(count);
}
function decrement() {
count--;
console.log(count);
}
function getCount() {
return count;
}
return {
increment,
decrement,
getCount
};
})();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.count); // undefined,拿不到
console.log(counter.getCount()); // 2
此模式称作 IIFE(立即执行函数),其创建出一个闭包,于其中的 count 变量,被返回的方法“记住”,外部不能够直接进行修改,仅可依照所提供的接口来操作。这是不是类似一个“保险箱”?钥匙仅给予了几个特定的人。
2. 函数工厂:批量生产定制函数
闭包能够被用以创建具备特定“预设”的函数,举例来说,存在一个能够记录调用次数的函数。
function createCounter(initial = 0) {
let count = initial;
return function() {
count++;
return count;
};
}
const counterA = createCounter(10);
console.log(counterA()); // 11
console.log(counterA()); // 12
const counterB = createCounter(0);
console.log(counterB()); // 1
每一个计数器,都单独有着自身的count变量,彼此之间不会产生干扰。这座工厂恰似制作定制蛋糕,每一位客户所拿到的,皆是属于自己独一无二的那一份。
3. 防抖与节流:控制函数执行频率
前端性能优化里较为常见的手法是防抖以及节流,它们的核心均借助闭包去对计时器以及状态进行保存。
用户连续触发事件之际,存在防抖情况,即唯有最后一次等待终结之后,才会予以执行这一操作,就像在搜索框进行输入时的那般情况。
function debounce(fn, delay) {
let timer = null; // 闭包保存timer
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用
const search = debounce(() => console.log('搜索中...'), 500);
节流:限制函数在单位时间内最多执行一次(比如滚动事件)。
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
这两个函数返回的皆是闭包,当中的timer或者last被“记住”了,因而每次调用的时候均能够访问到上一次的状态。
4. 柯里化:提前固定参数
柯里化,是一项技术,这项技术能将多参数函数,转变为一系列单参数函数,其本质,同样也是闭包。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...more) {
return curried.apply(this, args.concat(more));
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
依照返回新设函数的每一回,原本的那些args会被闭包留存下来,一直到参数全都凑齐了才会执行。恰似你向一家餐厅留下了订单,每一次打电话去添加菜品,最后一同进行结算。
5. 事件监听中的回调
事件回调之中去访问外部变量,实际上它就是闭包,举例来说,有一个简单的计数器按钮。
let count = 0;
document.getElementById('btn').addEventListener('click', function() {
count++;
console.log(count);
});
此处在的匿名函数铭记了外部的count变量,每一次点击都能够访问到最新的数值。
二、闭包的“阴暗面”:内存泄漏与性能
明明闭包这般香,为何却有人讲它不好呢?原因在于它会“赖着不走”,就是那些被记住的变量,哪怕外部函数已然执行完毕,也不会被进行垃圾回收,只要闭包函数还处于存活状态,它们便会一直存在。
1. 什么是内存泄漏?
程序使用完内存后,系统却未及时进行回收,致使内存占用持续增大,最终造成浏览器出现卡顿现象,甚至走向崩溃,这便是内存泄漏。
闭包导致泄漏的典型场景:
function leak() {
let bigData = new Array(1000000).fill('leak');
return function() {
console.log('I am a closure');
// 虽然没有直接使用bigData,但闭包还是引用了它
};
}
const closureFn = leak(); // 泄漏了100万个元素的数组
上边所举的那个例子里头,返回的那个函数尽管并未用到bigData,然而由于bigData跟它处于同一个作用域,闭包会留存整个作用域链上的全部变量。所以要是闭包始终存在,那些没有用处的变量也会一直占据内存。
2. 如何避免闭包导致的内存泄漏?
closureFn = null; // 这样bigData就可以被回收了
function good() {
let bigData = new Array(1000000).fill('data');
let needed = 'only me';
return function() {
console.log(needed); // 只引用needed,bigData会被回收
};
}
由于闭包仅仅引用了所需的,所以引擎能够进行优化,进而将大数据标记为不可达的。
3. 弱引用:救星Map和Set
ES6引入了WeakMap,以及WeakSet。它们有着这样的特性,其键是弱引用的。也就是说啊,要是键对象不再被其他处引用,那么即便它还存在于耳WeakMap里,依旧会被垃圾回收。
这在闭包中可以用来缓存数据,而不阻止回收。
const cache = new WeakMap();
function process(obj) {
if (!cache.has(obj)) {
const result = heavyComputation(obj);
cache.set(obj, result);
}
return cache.get(obj);
}
要是obj于别的地方被销毁掉了,cache当中的键值对也会自顾自地消失掉,不会导致泄漏情况的发生。
三、以实际操作来说:将闭包用于封装私有数据的最佳做法是,在并非需要完全隔离的情形下,闭包是有助于实现模块化的得力助手。然而在现代开发过程中,能够运用ES6模块(import/export)去替换IIFE,如此会更加清晰明了。防抖节流借助闭包来保存状态:这属于闭包的经典运用方式,并没有什么值得纠结之处。要谨慎地使用返回闭包的高阶函数:当闭包持有数量众多的数据时,务必要保证及时进行清理。善于运用let来替代var:let具备块级作用域,能够防止一些意外出现的闭包问题。于DevTools之中对内存加以监控,借助Chrome的Memory面板,能够拍摄快照,瞧瞧哪些闭包对象始终留存,用于辅助定位泄漏之处。四、进行总结,闭包乃是优秀员工,然而切勿使其996。
JavaScript里的闭包乃是强大特性,它赋予函数以“记忆”,可去实现模块化、柯里化、防抖节流这类高级功能。然而,还需留意它存在的副作用:被记住的变量并不会自动消逝,要是不加留意,极易引发内存泄漏。
记住几个原则:
要是掌握了闭包,那你便掌握了成为 JS 高级编程熟手的关键之道;明天,咱将会踏入 JS 的另外那极具灵性的领域,也就是原型跟原型链,瞧瞧那个能让初涉者心生怯意的概念,究竟是怎样些情况,到底是怎么一回事。
如若你认为今日所讲的闭包应用以及内存管理讲解得极为透彻,那就点个赞,以便让更多的人能够看到。要是存在疑问,就在评论区相见,我们明天再会!

相关标签: # 闭包 # JavaScript # 内存管理 # 模块化 # 性能优化