写好JavaScript的三大重要原则之过程抽象| 青训营笔记

96 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天 月影老师告诉我们写好JavaScript(包括其他语言)的三大重要原则:

  • ① 各司其责
  • ② 组件封装
  • ③ 过程抽象

过程抽象

  • 用来处理局部细节控制的一些方法
  • 函数式编程思想的基础应用

函数式编程是一种编程范式,也就是如何编写程序的方法论。

函数式编程有如下5个鲜明的特点:

  1. 函数是“第一等公民”
  2. 只用"表达式",不用"语句"
  3. 没有"副作用"
  4. 不修改状态
  5. 引用透明

此处介绍的函数式编程的概念及特点节选自阮一峰老师的函数式编程初探,感兴趣的读者可自行查阅。

案例

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

来看一个具体的例子:勾选任务后,任务自动消失

实现这个操作的JS代码:

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

问题:当我们重复多次点击同一个元素时,会报错,元素未消失,多次点击导致removeChild操作失败报错。

image.png

为了解决这个问题我们可以使用addEventListeneronce参数。让绑定的事件只在第一次点击的时候执行,之后点击都不执行。

Once

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

如图的开门操作,就可以抽象出来。 image.png

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

在代码中,我们把fn赋值给了ret,fn变为null,所以在第二次运行时由于fn为null,就没有办法再次执行。

如上案例我们就可以修改为:

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

高阶函数

定义

  • 以函数作为参数
  • 以函数作为返回值
  • 常用于作为函数装饰器

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

image.png

HOFO等价范式

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

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

Throttle

节流函数:每隔一段时间,只执行一次函数

相当于游戏中的技能cd时长

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

常见场景:

  • 窗口调整(resize)
  • 页面滚动(scroll)
  • DOM 元素的拖拽功能实现(mousemove)
  • 抢购疯狂点击(click)

Dehounce

防抖函数:多次事件一次响应

在事件被触发n秒后再执行函数,如果在这n秒内又被触发,则重新计时

function dehounce(fn,time){
let timer;
return function(){
clearTimeout(timer)
timer = setTimeout(()=>{
    fn.apply(this,args);
},time)
}
}

常见场景:

  • 输入框实时搜索联想(keyup/input)

consumer函数

相当于将同步操作变成一个异步的操作

实现相隔1秒执行一次add方法

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

function add(ref, x){
  const v = ref.value + x;
  console.log(`${ref.value} + ${x} = ${v}`);
  ref.value = v;
  return ref;
}

let consumerAdd = consumer(add, 1000);

const ref = {value: 0};
for(let i = 0; i < 10; i++){
  consumerAdd(ref, i);
}

实现快速点击慢慢执行:

btn.onclick = consumer((evt)=>{
  let t = parseInt(count.innerHTML.slice(1)) + 1;
  count.innerHTML = `+${t}`;
  count.className = 'hit';
  let r = t * 7 % 256,
      g = t * 17 % 128,
      b = t * 31 % 128;
  
  count.style.color = `rgb(${r},${g},${b})`.trim();
  setTimeout(()=>{
    count.className = 'hide';
  }, 500);
}, 800)

iterable

迭代器函数:将一个函数包装成迭代器函数

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

纯函数

思考:为什么要使用高阶函数?

首先我们要知道什么是纯函数。

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

例如:

const sum = (x, y) => x + y; 
sum(1, 2) // 3

输入参数只要是1和2,输出的结果就永远会是3 image.png

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

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

我们可以看到,每次的结果都不一样。

通过HOFO范式拓展出来的高阶函数都是纯函数

JS中数组的哪些API是纯函数? 【纯】concat、map、filter、slice 【非纯】push、pop、shift、unshift、forEach、some、every、reduce

总结

过程抽象 / HOF / 装饰器

  • 不仅可以对数据进行抽象,也可以对过程进行抽象
  • 对函数的操作可以用高阶函数抽象,利于复用且不修改原有函数代码(非侵入式)
  • 代码库中要多用纯函数

命令式 / 声明式

  • 命令式 关注怎么做 How
  • 声明式 关注做什么 What,有更强的扩展性
  • JavaScript既可以编写命令式的代码,也可以编写声明式的代码。
  • 多写声明式代码,抽象程度更高,拓展性更强