-
Notifications
You must be signed in to change notification settings - Fork 0
Description
函数柯里化被提过很多次,简单说它的概念是:只传递给函数一部分参数去调用它,让它返回一个函数去处理剩下的参数。
函数柯里化被提过很多次,简单说它的概念是:只传递给函数一部分参数去调用它,让它返回一个函数去处理剩下的参数。
第一版参考代码
var curry = function (fn) {
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
};因为一直没有搞懂[].slice.call(arguments)的用法,所以在思考curry函数之前,先来看看这个arguments,一直以来,我是这样使用这玩意的:
var test = function(x, y) {
console.log(...arguments);
}那么这样使用时,其实我是把它当成了一个数组(在ES8中,扩展运算符也可以用于类数组对象了),粗略的想一想,咦,这没什么错啊,这是一个数组,数组里的成员是依次对应着函数接受的参数。于是,我做了下面这个尝试:
var test = function(x, y) {
console.log(arguments.slice(0, 1))
}诶,诶...为啥,我想获取第一个参数结果报错了呢。难道,arguments不是一个数组?于是,我试着用了typeof arguments,果然,原来arguments并不是一个数组!而是一个类数组的对象!函数所接受的每一个参数在这个对象里都长这样:
{
'0': 1,
'1': 2,
...
}终于搞懂了arguments,其实这里[].slice.call(arguments,1)的目的是为了获取除了处理函数fn以外的参数,改为ES6的写法其实可以避免对于arguments的误解,具体写法为Array.from(arguments).slice(1)。
那么我们再来继续看第一版参考代码。
因为当还有剩余参数未被传入时,我们需要的不是报错,而是,返回一个函数去处理剩余的参数,第一版代码是这样写的
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
}详细点说,就是返回了一个匿名函数,匿名函数中数组newArgs整合了函数curry剩余所有的参数,然后再将这些参数传入fn,并返回了fn执行后的结果。
这个curry函数的用法是:

从结果来看,大致完成了我们的预想,可以通过curry()()这样的方法去处理第一次未传完的参数,但是这是远远不够的,我们尝试下面这种用法时,发现报错了!

从代码来看,出现这样的报错是很正常的。因为代码并没有考虑到超过两次传参的情况,只做了一次参数整合,那么我们来看看第二版参考代码。
第二版参考代码
function sub_curry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
return fn.apply(this, [...args, ...Array.from(arguments)]);
};
}
function curry(fn, length) {
length = length || fn.length;
return function() {
if (arguments.length < length) {
var combined = [fn].concat(Array.from(arguments));
return curry(sub_curry.apply(this, combined), length - arguments.length);
} else {
return fn.apply(this, arguments);
}
};
}来分析一下这段代码,函数sub_curry其实就是第一版代码,它的用途就是,接受并整合参数然后返回一个可以执行fn的函数,那么我们缺少的是什么,好吧,我们暂且称之为包裹函数吧,由名可知,它的作用就是用来判断,参数是不是真的传完了,当我们执行curry()()()()...,后面的括号可能数不清了,这个过程相当于,一层一层地剥开柚子皮,直到我们的参数传完了,再执行fn,那么,我们看一下这段代码是怎么实现这个功能的。我们先看一个应用这个curry函数的例子:
var fn = curry(function(a, b, c) {
return [a, b, c];
});
fn("a")("b")("c") // ["a", "b", "c"]先看第一步length = length || fn.length,它是用来获取传入的函数fn应该接受的参数数目,再看返回的匿名函数,if语句判断fn第一次执行时接受的参数数目和预期数目是否一致,在此处arguments.length是1,length是3,所以说明参数还没传完,通过数组combined缓存fn和执行时接受的参数,在此处是"a",接下来做的事情可想而知,就是递归,那么如何递归呢,我们的curry只接受两个参数fn和参数数目,参考代码给出了答案curry(sub_curry.apply(this, combined), length - arguments.length),分析一下在这里sub_curry的作用,聪明的你应该看出来了,作用就是去返回一个可以去执行的函数,目的是保留传入的fn和传入fn的参数而不去执行fn,类比的写法可以看成
var add = x => y => x + y在进行add(1)时,其实是将1这个参数做了个缓存,在执行add(1)(2)时再将1取出来使用,其原理其实就是闭包。参考代码最后传入的length - arguments.length就不用过多解释了,目的就是告诉接下来执行的curry,fn还剩多少个参数没有传完。
如此递归下去,直到arguments.length<length为 false 时,说明参数已经接受完毕,然后执行 fn.apply(this,arguments)。
第二版参考代码
第二版参考代码已经完全能满足我们的需求,但是柯里化是函数式编程的概念,所以上面的代码并不够纯,因为,它依赖于外部的包裹函数sub_curry ,那么接下来给大家看的是第三版参考代码:
function curry(fn, args) {
var length = fn.length;
args = args || [];
return function() {
var _args = [...args, ...arguments];
if (_args.length < length) {
return curry.call(this, fn, _args);
} else {
return fn.apply(this, _args);
}
}
}这个做法,简单来说就是把非fn的参数,直接合成一个数组,然后直接传递这个参数数组,其他的原理和之前的代码如出一辙,这样我们的curry函数就算完成了,而且够纯,并且比第二版代码更易懂。
那么,为什么要用柯里化函数呢?目的就是实现单一化参数输入,避免一次传入多个参数。在实际的应用中,以ajax请求为例,我们可以有效的从后端请求到的数据中,根据不同的输入情况得到我们最后想要的结果,具体应用方面的介绍,等我有空再写笔记吧~~~


