这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战。
过程抽象是⽤来处理局部细节控制的⼀些⽅法,是函数式编程思想的基础应⽤。
一个例子:Todo List
实际业务中我们经常需要限制用户的操作次数,比如一次性的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);
});
});
如图,我们的todo list在点击完成时会有一个2秒钟的淡出动画,但是如果用户在动画未结束时,又去点击该按钮,就会报一个错:
所以我们要让函数只执行一次,同时为了让这个只执行一次的需求覆盖不同的事件处理,我们可以将这个需求剥离出来,也就是过程抽象。
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,调用fn跟HOF0(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既可以写命令式的代码,也可以写声明式的代码,处理复杂逻辑时,推荐使用声明式。
一个例子:Toggle
画一个开关,点击切换开关状态。
命令式
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'
);