javascript里的柯里化函数

Amber ·
更新时间:2024-11-13
· 626 次阅读

柯里化(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 )
作者:皮蛋不是狗



函数 柯里化 JavaScript

需要 登录 后方可回复, 如果你还没有账号请 注册新账号