聊一聊javaScript的高阶函数

180 阅读3分钟

典型例子:节流与防抖

简单的防抖节流

节流与防抖大家应该都再熟悉不过了,下面我分别列出简单的防抖与节流。

// 防抖
function debounce(func,wait){
  let timer=null;
  return function(){
    console.log(timer)
    if(timer){
      clearTimeout(timer);
    }
    timer=setTimeout(() => {
      func.apply(this,arguments);
    }, wait);
    
  }
}
// 节流
function myThrottle(func, wait) {
  let flag=true;
  return function(...args){
    if(!flag) return;
    flag=false;
    func.apply(this,args);
    setTimeout(() => {
      flag=true;
    }, wait);
  }
}

但是其实还有很多情况我们没有考虑到,如果在某些特定的需求下使用上述代码是不行的,比如说一个输入事件,我们使用简单的防抖,wait设置为3s,此时对应的规则为用户输入→用户停止,等待3s,执行func

可控的防抖节流

但是现在我们有一个奇怪的需求,我们需要用户在输入的时候能够立马执行func,之后再进行用防抖,也就是说现在我们的规则变成了:用户输入(立马执行func)→用户停止,等待3s,执行func,n秒过后,用户输入(立马执行func)→用户停止,等待3s,执行func....,此时我们需要再次封装我们的函数,使得这种奇怪的需求变得可控。

在下列代码中,我们额外添加了一个可选的option对象,在该对象里控制开始与结尾是否执行func

// 可控制的防抖
//leading: 开始时是否执行,trailing: 结束时候是否执行
function debounce(func,wait,option ={leading:false,trailing:true}){
  let timer=null;
  return function(){
    let isInvoked=false;
    //没有定时器(本次轮询开始)且leading为true
    if(timer == null && option.leading){
      func.apply(this,arguments);
      isInvoked=true;
    }
    clearTimeout(timer);
    timer=setTimeout(() => {
      //本次输入事件开始时未执行过func且trailing为true
      if(option.trailing && !isInvoked){
        func.apply(this,arguments);
      }
      timer=null;
      }, wait);
  }
}
//可控制的节流
function throttle(func,wait,option = {leading: true,trailing: true}){
  let flag=true; 
  let preArgs=null;
  function setTime(){
   //trailing为true,一直执行定时器
    if(preArgs && option.trailing){
      func.apply(this,preArgs);
      preArgs=null;
      setTimeout(setTime, wait);
    }else{
      flag=true;
    }
  }
  return function(){
    if(!flag){
      preArgs=[...arguments];
    }else{
      if(option.leading){
        func.apply(this,arguments);
      }
      flag=false;
      setTimeout(setTime,wait)
    }
  }
}

上述代码我们额外添加了一个可选的option对象,来进一步控制防抖与节流的行为,现在我们只需要通过设置{leading:true,trailing:true}来实现: 用户输入(立马执行func)→用户停止,等待3s,执行func,等待n秒,用户输入(立马执行func)→用户停止,等待3s,执行func

可以点此查看更完整的可控的防抖解析可控的节流解析

柯里化

简单柯里化

const join = (a, b, c) => {
   return `${a}_${b}_${c}`
}

//柯里化join函数
const curriedJoin = curry(join)

curriedJoin(1, 2, 3) // '1_2_3'

curriedJoin(1)(2, 3) // '1_2_3'

curriedJoin(1, 2)(3) // '1_2_3'

柯里化也是js中比较常用的概念,现在我们来尝试写出一个 curry 函数,实现上面的效果,接收一个函数然后将该函数柯里化后并返回。


function curry(func) {
  return function innerFunc(...args) {
    //func为原函数,innerFunc为新函数
    //如果新函数的参数比原函数多或相等则执行原函数
    if(args.length >= func.length){
      return func(...args)
    }else{
    //否则一直累加新函数的参数
      return function(...next){
        return innerFunc(...args,...next)
      }
    }
  }
}

或者


function curry(func) {
  return function innerFunc(...args) {
    if(args.length >= func.length){
      return func(...args)
      //return func.apply(this,args)
    }else{
      //这里的bind作用是参数继承而不是改变this(我们应该清楚function内this都为window)
      return innerFunc.bind(this,...args)
    }
  }
}

点此查看更完整的柯里化解析

支持占位符

现在我们已经实现了一个简单的curry函数,但是现在我想让我们的curry函数能够支持占位符,应该怎么做呢?

const  join = (a, b, c) => {
   return `${a}_${b}_${c}`
}

const curriedJoin = curry(join)
const _ = curry.placeholder

curriedJoin(1, 2, 3) // '1_2_3'

curriedJoin(_, 2)(1, 3) // '1_2_3'

curriedJoin(_, _, _)(1)(_, 3)(2) // '1_2_3'



function curry(fn) {
  return function innerFunc(...args){
    //如果参数都不是placeholder且长度满足,执行fn
    const complete=args.length >= fn.length && !args.slice(0,fn.length).includes(curry.placeholder);
    if(complete){
      return fn.apply(this,args);
    }else{
      return function(...next){
        //依次替换上一个函数的placeholder
        const res=args.map(arg => arg === curry.placeholder && next.length ? next.shift() : arg);
        return innerFunc(...res, ...next);
      }
    }
  }
}
curry.placeholder = Symbol()

补充

事实上,在js中这些封装函数,高阶函数,我们使用的非常多,如扁平化,链式调用,pipe,EventEmitter,memo等等,在这里也给大家推荐一个关于javascript,typescript的题库,可以多刷一刷。

上述两个例子摘自本人github,大家感兴趣的可以自行观看。