以下是改写后的句子:具有构建模块化代码作用的闭包,于JavaScript里,却是有可能致使内存泄漏的暗藏风险之处;对其应用情景以及回收机制予以正确领会,那会让你的代码既具备优雅性又拥有高效性。
在尚无 ES6 模块的时期,靠立即执行函数(IILF)来实现真正私有属性的是闭包,开发者把内部变量以及函数包在函数作用域里,仅返回一个含有一些公共方法对象,凭借这种手段外部不能直接去访问内部数据,仅能借助暴露出来的接口去操作,以此保证数据安全性,现今 ES6 的 class 加上模块语法算是比较简洁些,然而闭包所实现的私有性在底层逻辑方面依旧被广为采纳。
现如今的前端框架里头,闭包始终起着模块化的作用,举例来说,Vue的响应式数据,还有React的useState钩子,实际上都是借助闭包去留存状态,在组件函数运行结束之后,其内部所定义的变量理应被销毁,然而返回的函数或者对象持有对这些变量的引用,致使它们能够持续存在并且随着时间而更新,这样的模式让状态管理变得可被预测而且易于追踪。
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
函数工厂是一种经典模式,什么经典模式,是利用闭包批量生成具有特定行为的函数的那种模式,怎么样批量生成,通过将预设参数保存在闭包中,如此这般,每次调用工厂函数都能返回一个携带固定配置的新函数,举个例子来说,比如创建不同折扣率的计算器,在这个例子里,工厂函数接收折扣比例, 返回的函数会记住这个比例,并且在后续计算中直接使用,利用这种产生诸多性质现象的模式避免了重复传递相同参数的冗余。
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
柯里化属于函数工厂的更高级形态,它会把多参数函数转变为一系列单参数函数嵌套,每次调用返回的新函数借助闭包收集已传入的参数,直至参数数量符合原函数要求才开展计算,此情况在函数式编程里颇为多见,能让代码复用性显著提高,比如在日志处理、数据校验等场景中,能够事先固定部分参数,而后依据具体场景补充剩余参数。
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);
前端性能优化里头,防抖与节流是两项高频运用的技术,它们的底层全都依靠闭包去保存计时器状态,防抖保证在连续触发事件之后,唯有最后一回等待期满才开展回调,常常被用于搜索框输入建议,闭包当中的setTimeout以及clearTimeout使得每次触发都能够重置计时器,确保最终仅仅执行一回,节流则限定函数在固定时间之内的执行次数,滚动事件监听是其典型场景。
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变量记录上次执行时间,结合当前时间判断是否要执行。这样的模式使状态在多次调用间持续存在,是实现性能优化不可缺少的工具。
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
根本原因是闭包致使内存泄漏,在于作用域链的引用保持,当函数返回内部函数时,即便返回的函数没使用外部函数的所有变量,引擎依旧可能保留整个父级作用域,特别是闭包被长期持有,像挂载在全局对象、DOM事件监听未移除,那些本应释放的变量就无法被垃圾回收器识别为无用内存,进而堆积成泄漏。
在Chrome开发者工具里,Memory面板的堆快照功能能够协助定位泄漏源头,借助对比操作开始与结束时各内存快照,可筛选出未被回收对象,进而查看其引用链。要是发觉大量闭包关联对象持续存在,就得检查是否及时移除事件监听,又或者是否把闭包赋给全局变量,另外还要看是否在定时器回调里意外留存大型数据。
let count = 0;
document.getElementById('btn').addEventListener('click', function() {
count++;
console.log(count);
});
ES6所引入的WeakMap以及WeakSet,为闭包内存管理给予了新的解决办法。它们所持有之键乃弱引用,也就是说要是键对象不再被其他地方予以引用,即便它存在于WeakMap里,也不会阻碍垃圾回收。在闭包中对DOM元素或者临时对象进行缓存时,运用WeakMap去替代普通对象或者Map,能够确保缓存不会变成内存释放的妨碍。
现今的JavaScript引擎,针对闭包作用域展开了优化。一旦引擎察觉到闭包仅仅引用了父函数里的部分变量,便会巧妙地仅仅留存被引用的变量,而不是整个作用域链。开发者能够借助合理拆分函数、防止在闭包中保存巨型数据,以此辅助引擎进行优化。另外,运用let和const去替代var,能够借助块级作用域缩减变量暴露范围,进而减少意外闭包的出现。
function leak() {
let bigData = new Array(1000000).fill('leak');
return function() {
console.log('I am a closure');
// 虽然没有直接使用bigData,但闭包还是引用了它
};
}
const closureFn = leak(); // 泄漏了100万个元素的数组
在实际的项目当中,当运用闭包去封装私有数据之际,应当优先去考量现代的模块化方案。针对于那些需要长期留存状态的场景,像是防抖节流函数,务必要保证在组件卸载或者不再被需要之时,把闭包引用设置为null,以便让垃圾回收机制能够及时地进行处理。在使用class去替代基于闭包的构造函数的时候,同样要留意及时地解除事件监听,防止闭包链有所残留。
运用DevTools的Performance面板去录制内存变化曲线,能够直观地对闭包对象的生命周期予以观察。在代码审查阶段,着重关注那些返回函数的高阶函数,核查其捕获的外部变量是否为必要的。针对缓存类场景,优先选用WeakMap,并设定恰当的缓存清理策略。要牢记这么一点:闭包尽管强大,然而只在真正有需要的时候才去创建,在使用之后及时释放引用。
closureFn = null; // 这样bigData就可以被回收了
在JavaScript里,闭包宛如一把双刃剑,若运用得当,能够编写出具备高复用特性、低耦合特质的优雅代码,要是运用不当,那么就有可能引发性能方面的问题。你可曾在项目里由于闭包遭致内存泄漏而踏入过陷阱?欢迎于评论区域分享你的经历以及排查的技巧。
function good() {
let bigData = new Array(1000000).fill('data');
let needed = 'only me';
return function() {
console.log(needed); // 只引用needed,bigData会被回收
};
}

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