昨天我们认识了闭包——那个“虽然离开了家但还记得家里密码”的神奇函数。今天咱们来深挖一下闭包这玩意儿到底能干啥有没有什么副作用怎么防止它把内存吃光看完这篇你不仅知道闭包怎么用还能在面试官面前侃侃而谈。前言闭包就像一个“赖着不走”的租客。你以为人走了结果他还留着你的钥匙时不时回来拿点东西。这在JavaScript里有时候特别好用有时候又特别坑。今天我们就来盘点闭包的几个经典应用场景顺便聊聊怎么让它“体面退场”别把你的内存吃光。一、闭包的应用场景这个“赖着不走”的家伙还挺有用1. 模块化私有变量与公共方法没有ES6模块之前闭包是JS实现模块化的主要手段。它能把内部细节藏起来只暴露需要公开的接口。constcounter(function(){letcount0;// 私有变量外面访问不到functionincrement(){count;console.log(count);}functiondecrement(){count--;console.log(count);}functiongetCount(){returncount;}return{increment,decrement,getCount};})();counter.increment();// 1counter.increment();// 2console.log(counter.count);// undefined拿不到console.log(counter.getCount());// 2这个模式叫IIFE立即执行函数它创建了一个闭包里面的count变量被返回的方法“记住”了外部无法直接修改只能通过提供的接口操作。像不像一个“保险箱”钥匙只给了你几个特定的人。2. 函数工厂批量生产定制函数闭包可以用来创建带有特定“预设”的函数比如一个能记录调用次数的函数。functioncreateCounter(initial0){letcountinitial;returnfunction(){count;returncount;};}constcounterAcreateCounter(10);console.log(counterA());// 11console.log(counterA());// 12constcounterBcreateCounter(0);console.log(counterB());// 1每个计数器都独立拥有自己的count变量互不干扰。这个工厂就像是做定制蛋糕每个客户拿到的是自己专属的那一份。3. 防抖与节流控制函数执行频率防抖和节流是前端性能优化的常见手法它们的核心都依赖闭包来保存计时器和状态。防抖用户连续触发事件时只有最后一次等待结束后才执行比如搜索框输入。functiondebounce(fn,delay){lettimernull;// 闭包保存timerreturnfunction(...args){if(timer)clearTimeout(timer);timersetTimeout((){fn.apply(this,args);timernull;},delay);};}// 使用constsearchdebounce(()console.log(搜索中...),500);节流限制函数在单位时间内最多执行一次比如滚动事件。functionthrottle(fn,delay){letlast0;returnfunction(...args){constnowDate.now();if(now-lastdelay){lastnow;fn.apply(this,args);}};}这两个函数返回的都是闭包里面的timer或last被“记住”了所以每次调用都能访问到上一次的状态。4. 柯里化提前固定参数柯里化是把多参数函数变成一系列单参数函数的技术本质也是闭包。functioncurry(fn){returnfunctioncurried(...args){if(args.lengthfn.length){returnfn.apply(this,args);}else{returnfunction(...more){returncurried.apply(this,args.concat(more));};}};}functionadd(a,b,c){returnabc;}constcurriedAddcurry(add);console.log(curriedAdd(1)(2)(3));// 6console.log(curriedAdd(1,2)(3));// 6每次返回新函数时原来的args被闭包保存直到参数凑齐才执行。就像是你给一家餐厅留了订单每次打电话加菜最后一起结算。5. 事件监听中的回调在事件回调里访问外部变量其实也是闭包。比如一个简单的计数器按钮letcount0;document.getElementById(btn).addEventListener(click,function(){count;console.log(count);});这里的匿名函数“记住”了外部的count变量每次点击都能访问到最新的值。二、闭包的“阴暗面”内存泄漏与性能闭包这么香为什么还有人说它不好因为它会“赖着不走”——那些被记住的变量即使外部函数已经执行完了也不会被垃圾回收只要闭包函数还活着它们就一直存在。1. 什么是内存泄漏内存泄漏就是程序用完了内存但系统没有及时回收导致内存占用越来越大最后浏览器变卡、甚至崩溃。闭包导致泄漏的典型场景functionleak(){letbigDatanewArray(1000000).fill(leak);returnfunction(){console.log(I am a closure);// 虽然没有直接使用bigData但闭包还是引用了它};}constclosureFnleak();// 泄漏了100万个元素的数组上面这个例子中返回的函数虽然没有用到bigData但因为bigData和它在同一个作用域闭包会保留整个作用域链上的所有变量。所以如果闭包一直存在那些无用的变量也一直占用内存。2. 如何避免闭包导致的内存泄漏用完后解除引用把闭包函数的变量置为null。closureFnnull;// 这样bigData就可以被回收了只保留需要的变量如果闭包中只用到部分变量可以用let声明在闭包外部提前“过滤”。functiongood(){letbigDatanewArray(1000000).fill(data);letneededonly me;returnfunction(){console.log(needed);// 只引用neededbigData会被回收};}因为闭包只引用了needed引擎可以优化把bigData标记为不可达。避免在循环中创建闭包除非必要因为循环中的闭包可能会意外持有大量变量。3. 弱引用救星Map和SetES6引入了WeakMap和WeakSet它们的键是弱引用的——如果键对象不再被其他地方引用那么即使还在WeakMap里也会被垃圾回收。这在闭包中可以用来缓存数据而不阻止回收。constcachenewWeakMap();functionprocess(obj){if(!cache.has(obj)){constresultheavyComputation(obj);cache.set(obj,result);}returncache.get(obj);}如果obj在其他地方被销毁了cache里的键值对也会自动消失不会造成泄漏。三、实战闭包的最佳实践用闭包封装私有数据在不需要完全隔离的情况下闭包是模块化的好帮手。但现代开发可以用ES6模块import/export替代IIFE更清晰。防抖节流用闭包保存状态这是闭包的经典应用没啥好纠结的。谨慎使用返回闭包的高阶函数如果闭包持有大量数据确保及时清理。善用let替代varlet有块级作用域能避免一些意外的闭包问题。在DevTools里监控内存用Chrome的Memory面板可以拍快照看看哪些闭包对象一直存在帮助定位泄漏。四、总结闭包是个好员工但别让它996闭包是JavaScript的强大特性它让函数拥有了“记忆”能实现模块化、柯里化、防抖节流等高级功能。但也要注意它的副作用被记住的变量不会自动消失如果不注意容易造成内存泄漏。记住几个原则用完闭包及时解除引用。在闭包里只引用需要的变量减少内存占用。现代开发中能用ES6模块就用模块减少手动闭包模式。遇到缓存场景优先考虑WeakMap。掌握了闭包你就掌握了JS高级编程的核心钥匙。明天我们将走进JS的另一个灵魂领域——原型和原型链看看那个让新手望而生畏的概念到底是怎么一回事。如果你觉得今天的闭包应用和内存管理讲得透彻点个赞让更多人看到。有疑问评论区见我们明天见
闭包:那个“赖着不走”的家伙,到底有什么用?
昨天我们认识了闭包——那个“虽然离开了家但还记得家里密码”的神奇函数。今天咱们来深挖一下闭包这玩意儿到底能干啥有没有什么副作用怎么防止它把内存吃光看完这篇你不仅知道闭包怎么用还能在面试官面前侃侃而谈。前言闭包就像一个“赖着不走”的租客。你以为人走了结果他还留着你的钥匙时不时回来拿点东西。这在JavaScript里有时候特别好用有时候又特别坑。今天我们就来盘点闭包的几个经典应用场景顺便聊聊怎么让它“体面退场”别把你的内存吃光。一、闭包的应用场景这个“赖着不走”的家伙还挺有用1. 模块化私有变量与公共方法没有ES6模块之前闭包是JS实现模块化的主要手段。它能把内部细节藏起来只暴露需要公开的接口。constcounter(function(){letcount0;// 私有变量外面访问不到functionincrement(){count;console.log(count);}functiondecrement(){count--;console.log(count);}functiongetCount(){returncount;}return{increment,decrement,getCount};})();counter.increment();// 1counter.increment();// 2console.log(counter.count);// undefined拿不到console.log(counter.getCount());// 2这个模式叫IIFE立即执行函数它创建了一个闭包里面的count变量被返回的方法“记住”了外部无法直接修改只能通过提供的接口操作。像不像一个“保险箱”钥匙只给了你几个特定的人。2. 函数工厂批量生产定制函数闭包可以用来创建带有特定“预设”的函数比如一个能记录调用次数的函数。functioncreateCounter(initial0){letcountinitial;returnfunction(){count;returncount;};}constcounterAcreateCounter(10);console.log(counterA());// 11console.log(counterA());// 12constcounterBcreateCounter(0);console.log(counterB());// 1每个计数器都独立拥有自己的count变量互不干扰。这个工厂就像是做定制蛋糕每个客户拿到的是自己专属的那一份。3. 防抖与节流控制函数执行频率防抖和节流是前端性能优化的常见手法它们的核心都依赖闭包来保存计时器和状态。防抖用户连续触发事件时只有最后一次等待结束后才执行比如搜索框输入。functiondebounce(fn,delay){lettimernull;// 闭包保存timerreturnfunction(...args){if(timer)clearTimeout(timer);timersetTimeout((){fn.apply(this,args);timernull;},delay);};}// 使用constsearchdebounce(()console.log(搜索中...),500);节流限制函数在单位时间内最多执行一次比如滚动事件。functionthrottle(fn,delay){letlast0;returnfunction(...args){constnowDate.now();if(now-lastdelay){lastnow;fn.apply(this,args);}};}这两个函数返回的都是闭包里面的timer或last被“记住”了所以每次调用都能访问到上一次的状态。4. 柯里化提前固定参数柯里化是把多参数函数变成一系列单参数函数的技术本质也是闭包。functioncurry(fn){returnfunctioncurried(...args){if(args.lengthfn.length){returnfn.apply(this,args);}else{returnfunction(...more){returncurried.apply(this,args.concat(more));};}};}functionadd(a,b,c){returnabc;}constcurriedAddcurry(add);console.log(curriedAdd(1)(2)(3));// 6console.log(curriedAdd(1,2)(3));// 6每次返回新函数时原来的args被闭包保存直到参数凑齐才执行。就像是你给一家餐厅留了订单每次打电话加菜最后一起结算。5. 事件监听中的回调在事件回调里访问外部变量其实也是闭包。比如一个简单的计数器按钮letcount0;document.getElementById(btn).addEventListener(click,function(){count;console.log(count);});这里的匿名函数“记住”了外部的count变量每次点击都能访问到最新的值。二、闭包的“阴暗面”内存泄漏与性能闭包这么香为什么还有人说它不好因为它会“赖着不走”——那些被记住的变量即使外部函数已经执行完了也不会被垃圾回收只要闭包函数还活着它们就一直存在。1. 什么是内存泄漏内存泄漏就是程序用完了内存但系统没有及时回收导致内存占用越来越大最后浏览器变卡、甚至崩溃。闭包导致泄漏的典型场景functionleak(){letbigDatanewArray(1000000).fill(leak);returnfunction(){console.log(I am a closure);// 虽然没有直接使用bigData但闭包还是引用了它};}constclosureFnleak();// 泄漏了100万个元素的数组上面这个例子中返回的函数虽然没有用到bigData但因为bigData和它在同一个作用域闭包会保留整个作用域链上的所有变量。所以如果闭包一直存在那些无用的变量也一直占用内存。2. 如何避免闭包导致的内存泄漏用完后解除引用把闭包函数的变量置为null。closureFnnull;// 这样bigData就可以被回收了只保留需要的变量如果闭包中只用到部分变量可以用let声明在闭包外部提前“过滤”。functiongood(){letbigDatanewArray(1000000).fill(data);letneededonly me;returnfunction(){console.log(needed);// 只引用neededbigData会被回收};}因为闭包只引用了needed引擎可以优化把bigData标记为不可达。避免在循环中创建闭包除非必要因为循环中的闭包可能会意外持有大量变量。3. 弱引用救星Map和SetES6引入了WeakMap和WeakSet它们的键是弱引用的——如果键对象不再被其他地方引用那么即使还在WeakMap里也会被垃圾回收。这在闭包中可以用来缓存数据而不阻止回收。constcachenewWeakMap();functionprocess(obj){if(!cache.has(obj)){constresultheavyComputation(obj);cache.set(obj,result);}returncache.get(obj);}如果obj在其他地方被销毁了cache里的键值对也会自动消失不会造成泄漏。三、实战闭包的最佳实践用闭包封装私有数据在不需要完全隔离的情况下闭包是模块化的好帮手。但现代开发可以用ES6模块import/export替代IIFE更清晰。防抖节流用闭包保存状态这是闭包的经典应用没啥好纠结的。谨慎使用返回闭包的高阶函数如果闭包持有大量数据确保及时清理。善用let替代varlet有块级作用域能避免一些意外的闭包问题。在DevTools里监控内存用Chrome的Memory面板可以拍快照看看哪些闭包对象一直存在帮助定位泄漏。四、总结闭包是个好员工但别让它996闭包是JavaScript的强大特性它让函数拥有了“记忆”能实现模块化、柯里化、防抖节流等高级功能。但也要注意它的副作用被记住的变量不会自动消失如果不注意容易造成内存泄漏。记住几个原则用完闭包及时解除引用。在闭包里只引用需要的变量减少内存占用。现代开发中能用ES6模块就用模块减少手动闭包模式。遇到缓存场景优先考虑WeakMap。掌握了闭包你就掌握了JS高级编程的核心钥匙。明天我们将走进JS的另一个灵魂领域——原型和原型链看看那个让新手望而生畏的概念到底是怎么一回事。如果你觉得今天的闭包应用和内存管理讲得透彻点个赞让更多人看到。有疑问评论区见我们明天见