柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
它的思想是降低适用范围,提高适用性.
乍一看有点蒙,这么抽象,谁看得懂.别急,我们先看一个简单的累加例子
//柯里化之前
function add( a , b ){
return a + b
}
add( 1 , 2 ) //10
//柯里化后
function curryAdd( a ){
return function( b ){
return a + b
}
}
curryAdd(1)(2)//3
这两个函数的区别很明显,柯里化之前接收所有的参数,进行计算,然后把值返回…而柯里化之后,函数接收一个参数,并返回一个函数,用来接收下一个参数.对比还是很明显的.
有的人会说,那又有什么用呢?花里胡哨.别着急,我们再看一个例子…
function double(item){
return item * 2
}
function treble(item){
return item * 3
}
function formatArr( fn , arr ){
return arr.map( item => fn( item ) )
}
//数组的每一项都乘2
formatArr( double , [ 1 , 58 , 3 ] )
formatArr( double , [ 34 , 45 , 23 ] )
formatArr( double , [ 54 , 38 , 9 ] )
...
//数组的每一项都乘3
formatArr( treble , [ 51 , 2 , 75 ] )
formatArr( treble , [ 82 , 6 , 26 ] )
formatArr( treble , [ 56 , 46 , 84 ] )
上面的例子中创建了三个函数,double、treble、formatArr.
double和treble对传过来的参数进行处理然后返回 . formateArr是一个通用函数,用于不同的场景.乍一看这么写没什么问题,但是我们还是能够看到如果对double多次调用的时候每次都会把double传进去.这样完全没必要.
通用性的增强必然带来适用性的减弱。接下来我们进行柯里化改造:
function double(item){
return item * 2
}
function treble(item){
return item * 3
}
function curryFormate( fn ){
return function(arr){
return arr.map( item => fn( item ) )
}
}
// 数组每一项乘2
var doubleArr = curryFormate( double )
doubleArr( [ 1 , 58 , 3 ] )//等同于curryFormate( double )( [ 1 , 58 , 3 ] )
doubleArr( [ 34 , 45 , 23 ] )//同上
doubleArr( [ 54 , 38 , 9 ] )//同上
// 数组每一项乘3
var trebleArr = curryFormate( treble )
trebleArr( [ 51 , 2 , 75 ] )//等同于 curryFormate( treble )( [ 51 , 2 , 75 ] )
trebleArr( [ 82 , 6 , 26 ] )//同上
trebleArr( [ 56 , 46 , 84 ] )//同上
这样看起来是不是很爽,不用每次都把double或者treble传进去.同时也提高了代码的合理性,而且也符合了它的思想-----降低适用范围,提高适用性
也暴漏了柯里化的一个好处----参数复用
接下来进行实战,这里有一道比较经典的题
// 实现一个add方法,使其满足如下预期
add(1)(2)(3) == 6
add(1,2)(3,4)(5) == 15
add(7)(8,9,10)(3) == 37
猛的一看没啥头绪,我们拆开看看
add(1)(2)(3)
//拆开看看
var add1 = add(1)
var add2 = add(2)
var add3 = add(3)
那我们发现每次传过去参数后,都会把参数存起来并没有直接相加,而是等我们把参数传完以后,在进行累加.又发现了柯里化的一个好处------延迟运行.
有的同学会想,函数的执行,执行完就销毁,之前的数据是怎么下来的,存在哪?
这个时候闭包就来了,什么是闭包?这里简单的解释一下
闭包函数:声明在一个函数中的函数,叫做闭包函数.它有什么特性呢?
正常情况下,我们声明一个函数,然后执行,执行完以后,没它什么事了,但是还在占用内存,这个时候,垃圾回收机制会把它‘清理掉’不要了,当然,随之而去的还有内部定义的变量啊等等.
但是,当我们声明了一个a函数,它返回了一个函数叫做b,b呢又拿到a的一些变量做了一些事情.这时候我们声明一个变量 c = a() 其实就是把b作为一个值赋给了c,当c执行的时候,也就是b执行的时候会用到a的变量.但是我们的a是不是已经执行过了呀?
按照之前的逻辑,a执行过就销毁,那么b中是不是拿不到属于a的变量了?所以这个时候不能销毁a函数.a函数得以保存,那么内部的变量也是一样.
所以我们可以利用闭包来保存add的参数
闭包函数:声明在一个函数中的函数,叫做闭包函数。它有什么特性呢?
正常情况下,我们声明一个函数,然后执行,执行完以后,没它什么事了,但是还在占用内存,这个时候,垃圾回收机制会把它‘清理掉’不要了,当然,随之而去的还有内部定义的变量啊等等.
但是,当我们声明了一个a函数,它返回了一个函数叫做b,b呢又拿到a的一些变量做了一些事情.这时候我们声明一个变量 c = a() 其实就是把b作为一个值赋给了c,当c执行的时候,也就是b执行的时候会用到a的变量.但是我们的a是不是已经执行过了呀?按照之前的逻辑,a执行过就销毁,那么b中是不是拿不到属于a的变量了?所以这个时候不能销毁a函数.a函数得以保存,那么内部的变量也是一样.
所以我们可以利用闭包来保存add的参数,参数保存好了,什么时候开始执行呢,是不是fn()就行了呀调一下这个时候参数的长度是0 ,一次可以作为判断条件,如果不满足的话就要继续返回当前函数了.
function add(){
//arguments 是一个伪数组,利用Array.prototype.slice.call将它转成一个真正的数组
var arg = Array.prototype.slice.call( arguments )
return function(){
if( !this.arguments.length ){
// 参数为0,进行累加并且返回
return arg.reduce( ( a , b ) => a + b )
}else{
// 接收参数,不满足执行条件,继续保存参数,并且返回当前函数
var _arg = Array.prototype.slice.call( arguments )
_arg.forEach( i => {
arg.push( i )
} )
// arguments的主要用途是保存函数参数,有一个callee属性,返回正被执行的 Function 对象
//已经不推荐使用arguments.callee();访问 arguments 是个很昂贵的操作,因为它是个很大的对象,每次递归调用时都需要重新创建。影响现代浏览器的性能,还会影响闭包。
//解决办法(给内部函数一个名字即可)笔者为了省力气这样写
return arguments.call
}
}
}
add(1)(2)(3)() //6
add(1,2)(3,4)(5)() //15
add(7)(8,9,10)(3)() //37
当然,如果不想在函数末尾加()让它执行的话,可以把累加的方法放在tostring上,利用对象的隐式特性来调用;
难道就没有一个通用的方法么?每次都得自己封装?别急,我们来封装一个通用的柯里化方法function curry( fn , length ){
//fn.length指的是fn的参数的length
var length = length || fn.length
return function(){
if( arguments.length >= length ){
// 如果参数长度大于等于length,不需要再接收参数,执行fn函数
return fn.apply( this, arguments )
}else{
//否则继续执行curry 这里参数的收集是跟bind的实现有关系,也是利用闭包,这个不懂的话可以看下bind的实现
return curry( fn.bind( this , ...arguments ) , length - arguments.length )
}
}
}
var add = curry( function(a,b,c){
return a + b + c
} )
var getArr = curry( function(a,b,c){
return [ a , b , c ]
} )
console.log(add(1,2)(3)); //6
console.log(getArr('a','b')('c')); //[ "a" , "b" , "c" ]
其实原理就是参数的长度判断和递归调用
通过判断参数的长度,如果参数长度符合fn的参数长度,那么就执行,否则就积累参数,重新调用匿名函数,再判断一遍.
还有一种es6的方法可以看下
var curry = fn =>
anonyMousFn = ( ...arg ) =>
arg.length >= fn.length ? fn(...arg) : ( _arg ) => anonyMousFn( ...arg , _arg )