闭包与防抖:代码里的“拖延症患者”和“隐私守护者”

110 阅读6分钟

今天学习的内容让我感觉自己像个魔法师,掌握了两种神奇的编程技巧:闭包赋予变量"隐身术",防抖则教会函数"拖延症"。让我们通过几个有趣的例子,揭开它们的神秘面纱!


场景一:输入框的"话痨症"治疗日记

想象两个输入框兄弟——InputA是个急性子,InputB则是个慢性子:

<input type="text" id="inputA">
<input type="text" id="inputB">

InputA每次按键都急吼吼地喊:"我要发请求!"(控制台惨不忍睹):

inputA.addEventListener('keyup', function(e) {
    ajax(e.target.value); // 疯狂输出
})

我们来看效果:

B1E595BDE5B620182338_converted.gif

而InputB就聪明多了,它请来了防抖这位"拖延症大师":

let debounceAjax = debounce(ajax, 200); // 200ms冷静期
inputB.addEventListener('keyup', function(e) {
    debounceAjax(e.target.value); // 优雅从容
})

有了防抖之后,我们在一定时间内连续输入,只执行最后一次,如果这是一个真正的请求后端数据的函数,那么相比于上述输入框A,性能是显著提升的,如果我们不做防抖处理,那么我们的用户的输入,就像成千上万的DDOS攻击,会致使我们的服务器瘫痪 1.gif

那么什么是防抖呢?

是一种编程实践技术,用于确保某个函数不会在短时间内被频繁调用。具体来说,当一个事件被触发时(比如窗口调整大小、滚动、按键或鼠标点击等),如果在设定的时间间隔内该事件再次被触发,则重新计时;只有当事件触发后在指定的时间间隔内没有再次触发时,才会执行相应的处理函数。这种方法可以有效地减少不必要的函数调用次数,从而提高性能和响应速度。

防抖的优点包括:

  1. 减少计算资源消耗:通过限制函数的调用频率,减少了CPU和内存的使用,特别是在处理高频率触发的事件(如resize, scroll, keypress等)时非常有效。
  2. 提升用户体验:避免了由于频繁触发事件而导致的界面卡顿现象,使得用户交互更加流畅。
  3. 防止服务器过载:在网络请求中应用防抖技术,可以减少不必要的请求发送,减轻服务器负担,尤其是在搜索框输入实时查询这种场景下特别有用。
  4. 简化逻辑处理:对于一些只需要最后一次操作结果的情况,使用防抖可以忽略中间状态,直接获取最终结果,简化逻辑流程。

防抖函数的魔法在于它的"拖延策略":

function debounce(fn, delay) {
    return function(args) {
        clearTimeout(fn.id); // 打断前一次施法
        fn.id = setTimeout(() => {
            fn(args); // 延迟执行
        }, delay);
    }
}

当继续触发,会检测定时器是否存在,存在就清除掉,也就是说,只有最后一次定时器的回调函数才会执行

我们可以来看看,实际应用场景,在浏览器中,当用户输入内容,有下拉搜索框提示用户,但当用户快速输入,就不会触发(也就是google 发家致富的 suggest 用户建议输入):

2.gif

防抖哲理:用户输入时就像在说rap,我们不需要每个音节都响应,等他们喘气的间隙再行动才最明智!


场景二:this丢失案发现场

当防抖遇到对象方法时,发生了离奇的this失踪案:

let obj = {
    count: 0,
    inc: debounce(function(val) {
        console.log(this);
        console.log(this.count += val); // this神秘消失!
    }, 500)
}

可以看到执行结果,this指向了我们全局对象window,这是因为,this丢失,我们定时器内部的函数被当作普通函数执行了,this也就会指向全局对象,如果大家不清楚this指向,可以看看这篇文章JavaScript中的this指向:从懵圈到豁然开朗的奇幻之旅 这是我之前在学习this指向写的一篇文章,希望对于正在学this指向的你有所帮助

image.png 破案的关键在于保存案发现场:

function debounce(fn, delay) {
    return function(args) {
        let that = this; // 案发现场快照!
        clearTimeout(fn.id);
        fn.id = setTimeout(() => {
            fn.call(that, args); // 用call还原现场
        }, delay);
    }
}

返回的函数就是我们对象中inc方法我们可以用that接住方法的this,也就是我们的对象,让我们定时器中的函数this指向我们的对象,这也就解决的this丢失问题

image.png

侦探笔记:箭头函数是this的保镖,普通函数是this的迷路小孩,记得用callapply送它回家!


场景三:计数器的隐私保卫战

这个计数器有点害羞,它的count变量不想被外人知道:

function CreateCounter(num) {
    let count = 0 // 藏在闭包里的秘密
    
    return {
        num: num,   // 公开部分
        increment: () => count++, // 操作接口
        getCount: () => {
            console.log('有人偷看我的日记!');
            return count;
        }
    }
}

const counter = CreateCounter(1);
counter.increment();
console.log(counter.count); // undefined - 隐私保护成功!
console.log(counter.getCount()); // 1 - 需经授权访问

闭包哲学:闭包就像保险柜,变量锁在里面,只通过特定接口访问。既保护隐私又提供控制!


闭包应用全家桶(知识点速记)

1. 记忆函数 - 函数得了"好记性"
2. 柯里化 - "分步骤喂食"函数
3. 防抖 - "等等党"函数代表
4. 节流 - "匀速运动"爱好者
5. 私有变量 - 变量的"隐身衣"
6. 偏函数 - 函数的"预制菜"

当然在这篇文章我还没有来得及介绍我们的节流,之后会继续学习给大家介绍

冷知识:防抖和节流的区别就像:

  • 防抖:等电梯(最后一个人进来才关门)
  • 节流:水龙头(匀速滴水)

防抖在实战中的奇妙变身

根据使用场景调整防抖策略:

// 经典防抖(发请求场景)
const searchDebounce = debounce(ajax, 300);

// 立即执行版(按钮提交场景)
function debounceImmediate(fn, delay) {
    let timer;
    return function() {
        if(!timer) fn.apply(this, arguments); // 首次立即执行
        clearTimeout(timer);
        timer = setTimeout(() => timer = null, delay);
    }
}

适用场景对照表

场景策略延迟
搜索建议经典防抖300ms
窗口resize经典防抖200ms
按钮提交立即执行版1000ms
无限滚动节流500ms

闭包与面向对象的"跨界合作"

闭包实现私有变量与Class语法对比:

// 闭包版(老派做法)
function CoffeeMachine() {
    let water = 100; // 私有
    
    return {
        brew: () => water -= 10
    }
}

// Class版(新潮做法)
class CoffeeMachine {
    #water = 100; // 私有属性
    
    brew() { this.#water -= 10; }
}

历史小剧场:JS早期没有class关键字时,程序员们用闭包伪造私有变量,就像用纸板造坦克——外表唬人,实际易燃!


总结:闭包防抖三定律

  1. 节能定律:防抖是函数的节能模式,避免无意义的性能消耗
  2. 隐私定律:闭包是变量的保险柜,钥匙由你掌控
  3. 上下文定律:丢失this时,call/apply就是时空传送门

最后记住这个防抖口诀:

"频繁事件别慌张,
闭包里面藏定时。
清除上次保当前,
延迟执行最明智!"

闭包和防抖的搭配就像咖啡和奶泡——分开也能用,合起来才完美!现在就去给你的项目加点"拖延症"和"隐私保护"吧~