【青训营】写好JS——过程抽象

191 阅读4分钟

这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

过程抽象是⽤来处理局部细节控制的⼀些⽅法,是函数式编程思想的基础应⽤。

image-20220126212851441

一个例子:Todo List

实际业务中我们经常需要限制用户的操作次数,比如一次性的HTTP请求,以及一些异步交互。

image

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');

buttons.forEach((button) => {
  button.addEventListener('click', (evt) => {
    const target = evt.target;
    target.parentNode.className = 'completed';
    setTimeout(() => {
      list.removeChild(target.parentNode);
    }, 2000);
  });
});

如图,我们的todo list在点击完成时会有一个2秒钟的淡出动画,但是如果用户在动画未结束时,又去点击该按钮,就会报一个错:

image-20220126215025027

所以我们要让函数只执行一次,同时为了让这个只执行一次的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,也就是过程抽象

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

我们向once()中传入一个函数,在返回值中运行并把它置为null,这样我们就用once本身的闭包实现了该功能,在button中调用即可:

buttons.forEach((button) => {
  button.addEventListener('click', once((evt) => {
    const target = evt.target;
    target.parentNode.className = 'completed';
    setTimeout(() => {
      list.removeChild(target.parentNode);
    }, 2000);
  }));
});

之后任何只能执行一次的函数都可以在外面包一层once()来实现,这样的函数也叫做高阶函数。

高阶函数

once()一样以函数作为参数,而且返回值也是函数的函数叫做高阶函数,也常作为函数装饰器使用。

Higher-Order Function中有一个等价范式HOF0,调用fnHOF0(fn)是完全等价的,其他的高阶函数都是基于这个范式做了一些拓展:

function HOF0(fn) {
  return function(...args) {
    return fn.apply(this, args);
  }
}

下面会介绍一些常见的高阶函数:

节流 Throttle

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

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

防抖 Debounce

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

function debounce(fn, dur) {
  dur = dur || 100;
  var timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, dur);
  }
}

Consumer

function consumer(fn, time) {
  let tasks = [],
      timer;

  return function (...args) {
    tasks.push(fn.bind(this, ...args));
    if (timer == null) {
      timer = setInterval(() => {
        tasks.shift().call(this)
        if (tasks.length <= 0) {
          clearInterval(timer);
          timer = null;
        }
      }, time)
    }
  }
}

Iterative

function iterative(fn) {
  return function (subject, ...rest) {
    if (isIterable(subject)) {
      const ret = [];
      for (let obj of subject) {
        ret.push(fn.apply(this, [obj, ...rest]));
      }
      return ret;
    }
    return fn.apply(this, [subject, ...rest]);
  }
}

纯函数

上文一直在说高阶函数,但是我们为什么要使用高阶函数呢?这里就需要知道纯函数的概念。

纯函数需要满足以下三点:

  • 相同输入返回相同输出
  • 无副作用
  • 不依赖于外部状态

也就是说【一个函数不依赖于上下文,不管什么时候调用,调用多少次,只要输入相同,输出就是相同的,这样的函数就是纯函数】。从这就可以看出,高阶函数都是纯函数

举个例子:

// 纯函数
function add(a, b) {
  return a + b;
}

// 非纯函数
let a = 6;
function add(b) {
  return a + b;
}

可以看出第二个函数,a改变时,输出就改变了,所以它不是纯函数。

纯函数的优势在于我们不需要上下文就可以直接进行单元测试,如果非纯函数,我们还需要构建上下文环境,所以我们要多写纯函数,多写高阶函数

编程范式

主要的编程范式分为两种:命令式和声明式,其中进一步细分面向过程,面向对象,逻辑式以及函数式编程。

命令式编程的主要思想是关注计算机执行的步骤,一步一步告诉计算机先做什么再做什么,就是关注怎么做(How)。

声明式编程是以数据结构的形式来表达程序执行的逻辑,它的主要思想是关注做什么(What),但不指定具体要怎么做。

JS既可以写命令式的代码,也可以写声明式的代码,处理复杂逻辑时,推荐使用声明式。

image-20220127164935964

一个例子:Toggle

画一个开关,点击切换开关状态。

GIF

命令式

switcher.onclick = function(evt){
  if(evt.target.className === 'on'){
    evt.target.className = 'off';
  }else{
    evt.target.className = 'on';
  }
}

声明式

function toggle(...actions) {
  return function (...args) {
    let action = actions.shift();
    actions.push(action);
    return action.apply(this, args);
  }
}

switcher.onclick = toggle(
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);

三态

声明式非常利于扩展,如果有新的需求只需要再加一个状态即可:

function toggle(...actions) {
  return function (...args) {
    let action = actions.shift();
    actions.push(action);
    return action.apply(this, args);
  }
}

switcher.onclick = toggle(
  evt => evt.target.className = 'warn',
  evt => evt.target.className = 'off',
  evt => evt.target.className = 'on'
);