高阶函数之柯里化函数
维基百科中的定义
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
解释有点抽象,可以代码实现下效果
//普通函数
function add(a,b){
return a + b;
}
add(1,2); // 结果 => 3
//柯里化后
function curryingAdd(a){
return function(b){
return add(a,b);
}
}
let sum = curryingAdd(1)
sum(2) //结果 => 3
//这里也可以利用到 bind() 返回一个新函数
let sum1 = add.bind(null,1)
sum1(2) //结果 => 3
这里你可能会有点疑惑,可能暂时看不出柯里化函数的用处,体验不到柯里化的精髓,别急,下面是关于柯里化函数的使用场景
柯里化函数的使用场景
参数复用
我们知道Object.prototype.toString()
可以获取每个对象的类型,并且不同的对象的toString()
的结果也是不一样的
普通版
function typeTest(dataType,obj){
return new RegExp(dataType,"gi")
.test(
Object
.prototype
.toString.call(obj)
)
}
typeTest("array",[1,2,3]) //true
typeTest("array","Hello") //false
typeTest("boolean",true) //true
Currying 化
function typeTest(dataType,obj){
return new RegExp(dataType,"gi")
.test(
Object
.prototype
.toString.call(obj)
)
}
//第一种
var isNum = (function(type){
return function(data){
return typeTest(type,data)
}
})("number")
isNum(1) //true
isNum([]) //false
//第二种
var isArr = typeTest.bind(null,"array")
isArr(1) //false
isArr([]) //true
如果使用场景一样,参数的形式有一些是没有变动的情况下,我们可以优先设置,然后后续直接传频繁变动的参数即可,从而达到参数复用,提高开发效率,调用起来更加方便
延迟计算
可以看下方的场景,很好地体现出延迟计算的优点
function add(){
if(!arguments.length){
return 0;
}
var args = [].slice.call(arguments);
return args.reduce(function(a,b){
return a + b;
})
}
//currying 延迟计算
function currying(){
var nums = [];
return function fn(){
if(arguments.length){
nums.push(...arguments)
return fn;
}
return add.apply(null,nums);
}
}
var sum = currying();
sum(1,2) //无结果 未真正求值
sum(10,5)(10)(666,233)//无结果 未真正求值
sum() //返回总计 927
上面的代码理解起来很容易, 就是「用闭包把传入参数保存起来, 当传入参数的数量足够执行函数时, 就开始执行函数。上面的 currying 函数是一种简化写法, 判断传入的参数长度是否为 0, 若为 0 则立刻执行函数并返回结果, 否则收集参数。以此实现延迟计算
动态创建函数
动态创建函数?什么意思呢?
其实就和我们写兼容一样的道理,例如我们的事件监听,除了IE低版本内核使用的是attachEvent()
和detachEvent
,主流浏览器基本使用addEventListener()
和removeEventListener()
注册和注销事件监听,所以我们在使用时,需要确保当前方法是否可以调用,如果没有该方法则调用另外一个方法或者自定义方法,以此确保我们程序的正常运行.
这样的话, 我们在后续继续调用该方法的时候,都会进行判断兼容处理流程, 但其实这是没有必要的, 因为第一次的判断选取的兼容方案就已经明确下来接下来我们要选取那一种兼容方案, 而不是每次调用都会去判断, 这种情况下就非常适合柯里化方案来处理了。即第一次判断之后,动态创建一个新的函数用于后续参数的传入,并返回这个新函数
拿事件监听的兼容场景来作案例,在DOM2级事件监听时,需要兼容主流浏览器和IE低版本浏览器(IE < 9),方法就是对浏览器的环境进行判断,看当前浏览器是否支持
function addEvent(type,el,fn,capture = false){
if(window.addEventListener){
el.addEventListener(type,fn,capture);
}else if(window.addachEvent){
el.addachEvent("on" + type,fn);
}
}
这种写法虽然也可以达到兼容效果,但就是每次调用都会去判断,这是沐浴必要的,那么有没有什么方法让她只判断就行呢?可以采用闭包和函数自执行(IIFE)来处理
const addEvent = (function(){
if(window.addEventListener){
return function(type,el,fn,capture = false){
el.addEventListener(type,fn,capture);
}
}else if(window.addachEvent){
return function(type,el,fn){
el.addachEvent("on" + type,fn);
}
}
})()
上面这种实现方案就是一种典型的柯里化应用,在第一次的 if...else if...
判断之后完成部分计算,动态创建新的函数用于处理后续传入的参数,这样做的好处就是之后调用就不需要再次计算了。
封装 currying 函数
说这么多,有没有通用的柯里化函数封装方法呢?
答案是: 当然有!
function currying(fn,len){
//捕获函数需要传入 len 个函数后才可执行
//第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
var len = len || fn.length;
return function(){
//判断新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
if( arguments.length >= len ){
//参数一致则执行函数
return fn.apply(this,arguments)
}else{
//不满足参数个数则递归 currying 函数传入 fn.bind() => 传入部分参数的新函数,并且重新定义新函数所需的参数个数
return currying(fn.bind(this,...arguments),len - arguments.length)
}
}
}
//测试
var test = currying(function(a,b,c){
return a + b + c;
})
test(1)()(2,3) // 6
test()(1,2)(3) // 6
text(1)(2)(3) // 6
Function.prototype.length
函数的length
属性属于ES6语法的,表示函数参数的个数,但是,有个需要注意的就是length
不一定就是函数的参数个数
来自MDN的说法
描述
length
是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。 形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。与之对比的是,arguments.length
是函数被调用时实际传参的个数。
Function
构造器的属性
Function
构造器本身也是个Function
。他的 length
属性值为 1
Function.length
属性的属性特性:
属性名 | 默认值
-|-|-
writable | false |
enumerable | false |
configurable | true |
Function.prototype
对象的属性
Function.prototype
对象的 length
属性值为 0
console.log(Function.length); /* 1 */
console.log((function() {}).length); /* 0 */
console.log((function(a) {}).length); /* 1 */
console.log((function(a, b) {}).length); /* 2 etc. */
console.log((function(...args) {}).length);
// 0, rest parameter is not counted
console.log((function(a, b = 1, c) {}).length);
// 1, only parameters before the first one with
// a default value is counted
所以如果要封装柯里化时,得注意这个坑,不然也可以手动传入参数个数
小结
通过定义,我们认识了什么是柯里化函数,柯里化函数主要应用于哪一些场景[参数复用
,延迟计算
,动态创建函数
]
定义
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
应用场景
- 参数复用
- 利用
Object.prototype.toString
判断对象的类型- 延迟计算
- 部分求和
- 动态创建函数
- 添加事件绑定
currying 函数的封装
用闭包将传入的参数保存起来,当传入参数的数量符合要求时则执行函数,否则则通过递归传入fn.bind()返回的新函数接收剩余的参数
函数参数的
length
length
是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。 形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
最后来道劲爆的题目
封装一个函数,需求如下
function sum(...){
//code block
}
sum(1,2)(3) //6
sum(1,2)(3)(4) //10
sum(1)(2)(3)(4)(5) //15
请思考下,不要立刻看答案,别人就不好玩咯 (_) (
w_w ... ... 嘘!别吵偶,让偶思考一下!
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
(ˇˍˇ) 嗯~,思考中
^^v 成功了,高兴地笑,在用胜利的手势
(ノ・ω・)ノヾ(・ω・ヾ) 你挑战成功了吗
function sum(){
//将参数转化出数组
var args = [].slice.call(arguments);
function add(){
args.push(...arguments)
return add;
}
add.toString = function (){
if(!args.length){
return 0;
}
return args.reduce(function(a,b){
return a + b;
})
}
return add;
}
console.log(sum(1,2)(3) == 6) //true
console.log(sum(1,2)(3)(4) == 10) //true
console.log(sum(1)(2)(3)(4)(5) == 15) //true