高阶函数定义
高阶函数是至少满足下列条件之一的函数
- 函数可以作为参数被传递
- 函数可以作为返回值输出
函数作为参数传递
把函数当作参数传递,可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。
支持异步执行是JS的一大特性,实现原理大致是满足控制条件时(比如:定时器时间已到、满足驱动事件条件等)JS会将回调函数callback进入异步队列,并在主程序空闲期,将异步队列的callback出队,在主程序执行环境进行。
常用情况:
- 定时器任务window.setTimeout(callback,time)
- 事件驱动 oDiv.onclick = callback,
- 异步IO NodeJS的fs模块
- ajax请求 $.ajax(url,callback)
- 一些数组、字符串方法 [1,2,3].sort(function(){return a-b})
函数作为返回值输出
函数返回一个可执行函数,意味着过程是可延续的。
常用情况:
- 函数柯里化
currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值
/**
* @method setUpClass 模拟批次设置
* @Description 选择一天批次,以及各批次的具体时间段
* @params 一天上下班的次数num {上班时间workingTime: 09:00, 下班时间closedTime: 14:00, 上班打卡时间段beforeWorking:30min,下班打卡时间段afterClosed:10min}
* @return [{workingTime,closedTime,beforeWorking, afterClosed}]
*/
function setUpClass(num:number = 1) : object{
return function (arr:Array<object>){
if(num !== arr.length || num < 1){
throw new Error('批次不匹配')
}else { // 对应批次
if(isGt24h(arr)){
throw new Error('班次时间不能超过24h')
}
for(let i=0; i < arr.length; i++) {
let curArr = arr[i]
let nextArr = arr[++i]
if(!isNextGtPrev(curArr.working,curArr.closed)){
throw new Error('下班时间要晚于上班时间')
}
if(nextArr) {
if(!isNextWorkingGtPrevClosed([curArr,nextArr])){
throw new Error('下一批次的上班时间要晚于上一批次的下班时间')
}
}
}
return arr
}
}
}
let result2 = setUpClass(2)([{working:'09:00',closed:'12:00'},{working:'13:00',closed:"17:00"}])
console.log(result2)
上面代码是Up主在项目建模时用柯里化函数来实现多次传参,控制程序分步执行,代码由ts写的,copy时注意转译。
- 泛化柯里化
Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。
有没有办法把泛化this的过程提取出来呢?uncurrying就是用来解决这个问题的。uncurrying主要用于扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。
uncurrying来自JavaScript之父Brendan Eich在2011年发表的一篇文章。
以下代码是 uncurrying 的实现方式之一:
Function.prototype.uncurrying = function () {
let _this = this
return function() {
let obj = Array.prototype.shift.call( arguments )
return _this.apply( obj, arguments )
};
};
我们来看一下调用Array.prototype.push.uncurrying(),会发生什么?
let push = Array.prototype.push.uncurrying()
let obj = {
"length":1,
"0":1
}
push(obj,2)
console.log(obj) // {0: 1, 1: 2, length: 2}
// Array.prototype.shift.call( arguments ),arguments中,obj被截取出来
// _this.apply( obj, arguments ),调用数组的push方法
另一种实现方法如下:
Function.prototype.uncurrying = function() {
let _this = this;
return function() {
return Function.prototype.call.apply(_this, arguments);
}
}
- 函数节流
在某些场景下,函数有可能被非常频繁的调用,而造成大的性能问题。
比如window.onresize事件,oDiv.onmousemove事件,这些事件触发的频率非常高,绑定的回调函数伴随着 DOM 操作,继而引发浏览器的重排与重绘,是非常消耗性能的。我们可以借助setTimeout来进行限流,限流方案有多种,提供一种throttle函数。
原理:将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。
throttle函数执行2个参数,第一个参数为需要被延迟执行的函数,第二个参数为延迟执行的时间,具体代码如下:
let throttle = function (fn, interval) {
let _self = fn,
timer,
firstTime = true
return function () {
let args = arguments, // 浏览器传入的e
_me = this // 调用对象
if(firstTime) {
_self.apply(_me,args)
return firstTime = false
}
if(timer) { // 有定时器任务是直接return
return false
}
timer = setTimeout(function () {
clearTimeout(timer)
timer = null
_self.apply(_me,args)
},interval || 500)
}
}
window.onresize = throttle(function () {
console.log(1)
},500)
- 分时函数
有些时候,某些函数确实是用户主动调用的,但因为一些客观的原因,这些函数会严重地影响页面性能。一个例子是创建WebQQ的QQ好友列表。列表中通常会有成百上千个好友,如果一个好友用一个节点来表示,在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千个节点。在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,看到的结果往往就是浏览器的卡顿甚至假死。
这个问题的解决方案之一是timeChunk函数,一种使用定时器分割循环的技术,为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。
timeChunk函数接受3个参数,第1个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,第3个参数表示每一批创建的节点数量,具体代码如下:
let timeChunk = function (ary,fn,count) {
let value,
timer
let start = function () {
for(let i = 0; i < Math.min(count || 1, ary.length); i++){
value = ary.shift()
fn(value)
}
}
return function () {
timer = setInterval(function () {
if(ary.length === 0){
return clearInterval(timer)
}
start()
},200)
}
}
我们进行测试,假如我们有1000个好友的数据,我们利用timeChunk函数,每一批只往页面中创建8个节点
let ary = []
for (let i = 1; i <=1000; i++){
ary.push(i)
}
let renderFriendList = timeChunk(ary, function (n) {
let oDiv = document.createElement('div')
oDiv.innerHTML = n
document.body.appendChild(oDiv)
},8)
renderFriendList()
- 高阶函数实现AOP
AOP(面向切面编程)可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。
使用AOP的方式给函数添加职责,是JavaScript一种非常特别和巧妙的装饰者模式实现。
Function.prototype.before = function(beforeFn){
let _this = this
return function(){
beforeFn.apply(this,arguments)
return _this.apply(this,arguments)
}
}
Function.prototype.after = function(afterFn){
let _this = this
return function(){
_this.apply(this,arguments)
afterFn.apply(this,arguments)
}
}
function buy(money,goods){
console.log(`花${money}买${goods}`)
}
buy = buy.before(function(){
console.log(`存20000元`)
});
buy = buy.after(function(){
console.log(`还剩下10000元`)
});
buy(10000,'mac pro')
// 存20000元
// 花10000买mac pro
// 还剩下10000元
装饰者模式更详细的介绍参考JavaScript设计模式-装饰者模式
小结
本文介绍了高阶函数在JavaScript中的常见应用,由于JavaScript语言自身的特点,闭包、高阶函数在JavaScript开发中应用极多,很多设计模式都是通过闭包和高阶函数实现的。
参考文献
《JavaScript设计模式与开发实践》