「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」
写在前面
高阶函数,听起来多么牛逼的名字啊,其实并没有听起来那么的触不可及。
本篇将带你逐步剖析高阶函数。
啥啥啥?
首先,我们先解释一下,这高阶函数是个啥:
-
函数可以作为参数被传递
-
函数可以作为返回值被输出
以上条件只需要满足一个,那就是高阶函数了(没想到吧~)
将函数作为参数传递
将函数作为参数来传递,这意味着我们可以将一些业务逻辑放在作为参数的函数中,从而分离业务代码中变化与不变的部分。
其中回调函数就是一个很常见的使用场景。
回调函数
在我们使用ajax的时候,回调函数的使用非常的常见。
当我们想要在获得ajax的返回后做一些事情的时候,因为不能确切的知道什么时候会拿到返回,因此常见的方式就是将要做的事情当成一个函数传入到ajax的请求中,待请求完成后执行callback方法。
看个🌰
var getUserInfo = function(id,cb){
$.ajax('http://xxx.com/getUserInfo?' + userId,function(data){
if(typeof cb === 'function'){
cb(data)
}
})
}
getUserInfo(666,function(data){
alert(data.userName)
})
在上面的例子中,我们可以看到,我们在获取到ajax请求后将获取到的用户名弹出。
其实,回调函数的应用不只有异步中使用。当某些操作不适合在某个函数中执行时,我们可以将这些操作抽成一个函数,并将这个函数作为参数传递个另一个函数,委托另一个函数来执行。
sort
Array.prototype.sort接受一个函数作为参数,该函数封装了对数组元素的排序规则。
从sort方法的用途我们可以看出,我们要做的事情是对数组进行排序,这点是固定不变的。
而变化的是排序的规则,我们将这部分封装在函数参数里,动态传入sort方法,从而使Array.prototype.sort的排序变得非常的灵活。
[1,4,3,2].sort(function(a,b){
return a-b
}) // [1, 2, 3, 4]
[1,4,3,2].sort(function(a,b){
return b-a
}) // [4, 3, 2, 1]
函数作为返回值输出
相比把函数当作参数传递,函数当作返回值输出的应用场景更多,也更能体现函数式编程的巧妙。
判断数据的类型
当我们要判断一个数据类型时,将其抽成一个函数。
var isString = function( obj ){
return Object.prototype.toString.call( obj ) === '[object String]';
};
var isArray = function( obj ){
return Object.prototype.toString.call( obj ) === '[object Array]';
};
var isNumber = function( obj ){
return Object.prototype.toString.call( obj ) === '[object Number]';
};
......
getSingle
我们用举一个单例模式的例子。
var getSingle = function(fn){
var ret
return function() {
return ret || (ret = fn.apply(this,arguments))
}
}
在这个例子中,我们既把函数作为参数传递,又让函数执行后返回来另一个函数。
我们来看看效果
var getScript = getSingle(function(){
return document.createElement('script')
})
var script1 = getScript()
var script2 = getScript()
console.log(script1 === script2) // true script1 和 script2 为创建的 script 标签
高阶函数实现AOP
我们需要先解释一下什么是AOP: 主要作用是将跟核心业务逻辑模块无关的功能抽离出来,也称作面向切面编程。
将这些功能抽离出来后,通过动态织入的方式掺入业务逻辑模块中,从而保持业务逻辑模块的纯净以及高内聚性。
通常我们在js中实现AOP,都是指把一个函数 动态织入 到另外一个函数之中,我们来举一个通过扩展Function.prototype来做到这一点的例子。
Function.prototype.before = function(beforefn){
var that = this // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this,arguments) // 执行新函数,修正 this
return that.apply(this,arguments) // 执行原函数
}
}
Function.prototype.after = function(afterfn){
var that = this
return function(){
var res = that.apply(this,arguments)
afterfn.apply(this,arguments)
return res
}
}
var fun = function(){
console.log(2)
}
fun = fun.before(function(){
console.log(1)
}).after(function(){
console.log(3)
})
fun()
// 1 2 3
这种使用AOP的方式来给函数添加职责其实也是js中一种特别巧妙的装饰者模式的实现。
后面有机会的话会给大家也说说装饰者模式。
其他应用
介绍一些常见的高阶函数的使用场景。
柯里化
先简单介绍一下柯里化,一个柯里化函数会接收一些参数,接收了这些参数后,函数不会立刻执行,而是会继续返回另一个函数,并将刚才传入的参数在函数形成的闭包中保存起来。等到函数被真正需要求值时,之前传入的所有参数会被一次性用于求值。
我们举个例子来方便理解
var address = (function(){
let res = ''
return function(){
for(let val of arguments){
res += val
}
return res
}
})()
var curring = function(fn){
let args = []
return function(){
if(!arguments.length){
return fn.apply(this,args)
}else{
args = [...args,...arguments]
return arguments.callee
}
}
}
address = curring(address)
address('中国')
address('江苏省')
address('无锡市')
address()
// '中国江苏省无锡市'
这样我们就完成了一个输入地址的柯里化函数,当调用address时,如果带上了参数则会将这些参数暂存起来,只有当不带参数时,才会进行真正的求值计算。
节流
当某些场景下,一些函数被频繁调用时,可能会造成很大的性能问题。
比如频繁点击按钮调用接口,window.onresize,scroll等。
思路:我们频繁触发这些事件时,可能只需要在某段事件内只触发1次,这就需要我们按时间段来忽略一些事件。
代码实现:
function throttle(fun, wait) {
let timeout = null;
return function() {
if (!timeout) { // 如果定时器不存在
timeout = setTimeout(function(){ // 设置 wait时间后触发
timeout = null; // 清除定时器
fun() // 执行方法
}, wait)
}
}
}
function test(){
console.log('test')
}
// 我们以浏览器滚动为例 1s 触发一次
window.onscroll = throttle(test, 1000);
最后
以上,就是本人对高阶函数的一些浅显的理解,欢迎各路大佬拍砖。
同时,向大家推荐一个我参与的 vue3的移动端MD风格组件库,欢迎大家 star & pr