常用高阶函数(HOF) - 单次执行、节流、防抖 | 青训营笔记

391 阅读6分钟

常用高阶函数(HOF) - 单次执行、节流、防抖 | 青训营笔记

🏝 前言

这是我参与「第四届青训营」笔记创作活动的的第4天😺

写好JavaScript,需要掌握过程抽象的思想。对于过程抽象,其既是用来处理局部细节控制的一些方法,也是函数式编程思想的基础应用。而 高阶函数(HOF) 便是过程抽象思想的体现之一。
在这篇笔记中,将记录三种常用的高阶函数,以及使用高阶函数的好处。

💡 高阶函数

概念

高阶函数 HOF(Higher-Order Function)

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器
function HOF(fn){
    return function(...args){
        return fn.appply(this,args);
    }
}

常用高阶函数

1.单次执行 (once)

在页面中有一按钮,要求为其添加事件监听器:当这个按钮被点击时,

  • 在控制台打印 "hello world!" ;
  • 并要求这个动作只执行一次,若之后按钮再次被点击,打印动作也不会执行。

image.png

我们通常使用的方法是 addEventListener,并且在可选参数options中将 once 设置为 true ,意为只执行一次。

const btn = document.getElementById('btn');
document.addEventListener('click',()=>{
    console.log('hello world!');
},{once:true});

但对于一些不能配置可选参数、但也有“只执行一次”需求的事件,我们要如何实现其需求?

🍊为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象

结合高阶函数的定义,我们可以针对“只执行一次”的需求定义一个 once() 高阶函数,并将要处理的事件作为函数传入其中:

function once(fn){
    return function(...args){
        if(fn){
            const ret = fn.apply(this,args);
            fn = null;
            return ret;
        }
    }
}

首先向传入一个作为参数的回调函数,得到一个其返回的 新函数
调用 新函数 时,会对函数指针进行检测:

  • 若指针非空,则执行该回调函数;

  • 否则无操作。

将回调函数执行一遍后,把指针置空;
当再次调用 新函数 时,由于函数指针指向空,所以无操作,达到只执行一次的目的。

🍏例子

以一个计算两参数之和的函数为例子,要求该函数只能执行一次:

const once_demo = once(function(a,b){
    return a+b;
});

console.log(once_demo(1,1));
console.log(once_demo(2,2));

可以观察到控制台的打印结果:

image.png

令该函数执行两次,实际上只有第一次有效果。
对于第一次传入的两个相同参数 1 ,打印结果为 2
对于第二次传入的两个相同参数 2 ,打印结果为 undefined ,因为这次并没有执行求和函数。

2.节流 (throttle)

还是以页面中的按钮为例子,我希望用户在点击这个按钮时,

  • 控制台中会打印 "hello world!" ;
  • 为按钮发挥作用添加冷却时间,如500ms。

这意味着 addEventListener 绑定的函数在执行一次后会进入500ms的冷却时间,当这一冷却时间结束后,点击按钮才能产生打印"hello world!"的效果;但下一次打印操作完成后,按钮又会再次进入冷却。
在冷却过程中,无论用户点击多少次按钮,都不会有打印的操作。

这样的限制条件称为节流(throttle)。下面是一个针对节流的高阶函数:

function throttle(fn,time = 500){
    let timer;
    return function(...args){
        if(timer == null){
            fn.apply(this,args);
            timer = setTimeout(()=>{
                timer = null;
            },time);
        }
    }
}

对于使用高阶函数throttle()生成新函数时,传入的参数除了包含被调用的回调函数外,还需要传入一个间隔的时间time,回调函数的执行便以time为时间间隔。具体时间可由使用者指定。

需要注意的是 setTimeout 函数的返回值:

image.png

对于代码段:

    timer = setTimeout(()=>{
                timer = null;
            },time);

timer的值为 setTimeout 函数的返回值时,timer非空,回调函数并不会执行;
但在 setTimeout 函数中,会以500ms(之前设置了time = 500)为间隔执行语句timer = null ,使得timer为空,回调函数得以执行。

3.防抖 (debounce)

页面上存在一个内容被监听的多行输入框,对于这一输入框:

  • 当输入框中的内容发生变化时,控制台实时打印输入框中的内容;
  • 但必须在用户无输入操作500ms后,才允许进行打印操作。

image.png

如果不添加 “用户无输入操作若干秒后才打印” 的限制条件,当输入框的内容稍有变化时,控制台便会马上打印;当内容变化的次数一多,控制台打印操作便会变得频繁,产生抖动。

对于一些不希望产生抖动的操作,则需要添加限制条件,称为 防抖(debounce) 。下面是一个针对防抖的高阶函数:

function debounce(fn,dur = 500){
    let timer;
    return function(...args){
        clearTimeout(timer);
        timer = setTimeout(()=>{
            fn.apply(this,args);
        },dur);
       
    }
}

参数dur等待无操作所持续(duration)的时间

每次在执行debounce()返回的新函数前,都要使用clearTimeout()方法取消setTimeout()的定时,即打断当前的等待间隔时间和执行规律,随后再重新设置一个定时器。
在新设置的定时器中,以dur为时间间隔执行回调函数。

🔎 为什么要使用高阶函数?

纯函数与非纯函数

纯函数 (pure function)

  • 对外部没有副作用
  • 效果可预期

非纯函数 (inpure function)

  • 对外部有副作用
  • 测试时要构造特定的环境、初始化和还原
  • 测试难度更大

⚠️非纯函数越多,可维护性就越差,所以尽量使用纯函数。

使用高阶函数的好处

  • 过程抽象
  • 高阶函数都是纯函数,效果可预期
  • 提升代码的可维护性

📝小结

这篇笔记记录了常用高阶函数中的单次执行(once)节流(throttle)防抖(debounce),以及高阶函数的定义、使用高阶函数的好处。除此之外,还对 纯函数(pure function)非纯函数(inpure function) 做了简单的介绍。
常用高阶函数除了文中介绍的三种外,还包含了如同步触发异步执行函数consumer()、迭代函数iterative()等。而对于如何写好JavaScript,还要下不少的功夫,其中便包含了对过程抽象思想的熟练掌握。💨

2022/7/27