过程抽象是⽤来处理局部细节控制的⼀些⽅法,是函数式编程思想的基础应⽤。
一个例子: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' );