js-过程抽象|青训营笔记

60 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天 maiami.jpg

月影老师告诉我们写好JavaScript(包括其他语言)的三大原则 ① 各司其责 ② 组件封装 ③ 过程抽象

1. 案例引入

限制操作

我们经常需要对操作次数进行限制,比如一些异步交互、一次性的HTTP请求

来看一个具体的需求:让用户勾选任务之后,任务会慢慢消失

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); 
    }); 
});

效果虽然可以呈现,但是会有一个问题,如果我们多次快速点击同一个button,就会出现下面的错误

image.png

元素没有消失,我们点击,它所绑定的事件就还会被触发,导致多次removeChild,所以会报错

解决这个问题我们要让绑定的事件“只执行一次”。

我们可以设置addEventListeneronce参数(IE不行)

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

Once一次性执行函数

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

once函数接收的参数是一个函数fn,返回的是一个新的函数,在返回的函数中,做了一件事,就是让fn只执行一次,第二次执行的时候给fn已经被赋值为null,就无法被再次执行了。

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

这样,我们就将一次执行这个过程抽象出来了,任何需要一次执行的函数,只要在外面包一层once就可以实现

2. 高阶函数

定义

我们来看看高阶函数的定义

以函数作为参数 或者 以函数作为返回值 的函数成为 高阶函数

上面说的一次性执行函数once就是一个高阶函数

JS中数组的哪些API是高阶函数?

everymapfilterforEachreducesort

HOF0 等价范式

HOF0是高阶函数的等价范式

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

fnHOF0(fn) 是完全等价的,无论参数、调用上下文怎么变化,他们都是等价的! 也就是说,执行fn与执行HOF0(fn)没有任何区别

可以看出我们的Once函数就是在HOF0上进行拓展出来的

常见高阶函数

除了Once函数,还有很多其他常用高阶函数

throttle函数

节流函数 将多次事件按照时间做平均分配触发

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

效果,连续多次点击只会每500ms记录一次

debounce函数

防抖函数 适合多次事件一次响应的情况

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

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]);
  }
}

3. 纯函数

为什么要用高阶函数呢?

在解释这个问题之前,我们先引出纯函数这个概念

定义

一个严格的纯函数,是具有确定性无副作用幂等的特点。也就是说,纯函数不依赖外部环境,也不改变外部环境,不管调用几次,不管什么时候调用,只要参数确定,返回值就确定。这样的函数,就是纯函数。

我们来看一个例子

纯函数

function add(a, b) {
    return a + b;
}
add(1,2) // 3
add(1,2) // 3

每次执行,结果都一样,改变顺序结果也都一样

非纯函数

let x = 10
function foo() {
    // 会改变函数上下文数据x
    return x++
}
function bar() {
    return x * 10
} 

foo() // 11
bar() // 110
bar() // 1100
foo() // 1101

每次执行,结果都不一样,改变次序结果也不一样

高阶函数都是纯函数

我们在进行单元测试的时候,如果是一个纯函数的话,我们可以不需要上下文环境直接进行测试,

而如果是非纯函数,我们还要构建好它的上下文环境

所以最佳实践就是 多写纯函数!!!

JS中数组的哪些API是纯函数?

【纯】concat、map、filter、slice

【非纯】push、pop、shift、unshift、forEach、some、every、reduce