首页 电报出售网站内容详情

闭包:那个“赖着不走”的家伙,到底有什么用?

2026-03-21 2 飞机号购买网站

以下是改写后的句子:具有构建模块化代码作用的闭包,于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 # 内存管理 # 模块化 # 性能优化