函数柯里化
首先看一下函数柯里化的定义: 函数柯里化指的是一个转化过程,在这个过程中,把一个接受多个参数的函数,转化成一个个嵌套的函数,这些嵌套的函数只接受一个参数。举一个简单的例子体验一下, 我们写一个add 函数,它接受两个参数,然后返回参数的和。
const add = (x,y) => x + y;
简单调用,add(2, 3) 返回5,没有什么可说了。 现在我们把这个函数进行柯里化。柯里化,每一个函数只接受一个参数,所以我们要先声明一个函数,它接受一个参数,假设x,
function addCurried(x){
}
由于add 接受两个参数,所以还剩一个参数y, 只有函数才能接受参数, 所以addCurried 还要返回一个函数接受参数 y.
function addCurried (x) { return function(y) { } }
这时两个参数都获取到了,就可以进行计算了。所以可以在返回的函数中直接写return x + y;
function addCurried (x) { return function(y) { return x + y; } }
let fn = addCurried(2);
console.log(fn)
所以还要调用一次
let result = fn(3);
console.log(result);
简化方式的调用
let result = addCurried(2)(3);
console.log(result);
现在我们一步一步地把add函数进行了柯里化,如果把这个过程进行封装,那么就可以对任意接受两个参数的函数进行柯里化。我们把这个函数命名为curry,它首先接受一个实行柯里化函数作为参数,然后再返回我们上面的addCurried函数
function curry(fn) { return function addCurried(x) { return function(y) { return fn(x, y) } } }
现在我们根据对两个参数的函数进行柯里化的过程来实现对接收任意参数的函数进行柯里化。这个函数还是命名为curry吧,它还是接受一个函数作为参数,然后返回一个函数,返回的函数是真正柯里化的函数。为了代码的健壮性,我们要对传入的参数是不是函数进行验证,如果不是,直接抛出错误,如果是返回函数。
let curry = function(fn) { if (typeof fn !== 'function') { throw new Error('只能对函数进行柯里化'); } return function curriedFn() { } }
在返回的函数curriedFn中, 要实现柯里化的过程,一步一步的把fn 转化成接受单个参数的函数 ,所以函数curriedFn 要开始接受用户的传参, 理论上,curriedFn 只接受一个参数,但实际使用过程中,如果用户一次性就向curriedFn函数中传入所有的参数,那怎么办?我们需要考虑这种情况,使程序得以正常运行,不至于出错。怎么判断呢? 函数有一个length 属性,可以获取到函数的形参个数, arg是函数接受的实参。 arg.length < fn.length 就是用户输入的参数小于函数实际上要接受的参数, 反之则是大于。
如果用户在调用curriedFn 函数传入的参数大于fn 需要的参数,这种情况比较简单,直接让fn 执行就好了。fn.apply(null, arg);
let curry = function(fn) { if (typeof fn !== 'function') { throw new Error('只能对函数进行柯里化'); } return function curriedFn(...arg) { // 当调用返回的函数时,如果用户传递进来的参数多于我们要实行柯里化的函数参数,那直接让函数执行。 if (arg.length < fn.length) { } else { return fn.apply(null, arg); // 函数直接执行 } } }
如果用户在调用curriedFn函数传入的参数小于fn需要的参数时, 比如,用户就是只传入了一个参数,那就是真正的实现函数柯里化的过程,这种情况也比较复杂,首先肯定是要返回一个函数,函数继续接受参数。
if (arg.length < fn.length) { return function(...otherArg) { } }
当用户调用返回的这个函数时,它会接着传递参数。这时又要判断传入的参数和执行柯里化的函数fn需要接受的参数进行比较,不过这次用户传入的参数要和上次传入的参数进行合并,然后再进行比较,因为用户第一次传递的参数是arg, 这次传入的参数是otherArg, 由于两个都是数组,所以使用concat 就可以了。(arg.concat(otherArg).length) < fn.length
如果大于就执行,如果小于继续返回函数。
if (arg.length < fn.length) { return function(...otherArg) { // 接着比较用户传入的参数和fn 函数要接受的参数进行比较, 不过这次参数要和上一次参数进行合并 if ((arg.concat(otherArg)).length < fn.length) { return function(...oOtherArg) { } } else { return fn.apply(arg.concat(otherArg)); } } }
如果用户还要传递参数时,我们接下来的操作是一样的,就是参数数量进行比较,大于就执行,小于就继续返回函数。不过这时用户传入的实际的参数又要合并这次传入的参数arg.concat(); 你会发现这个逻辑就是调用了一次curriedFn函数, 不过参数是arg.concat(otherArg)
if (arg.length < fn.length) { return function(...otherArg) { return curriedFn.apply(null, arg.concat(otherArg)); } }
这就形成了一个递归,不停地调用函数curriedFn, 递归的终止条件是arg.length < fn.length; 我们能不能达到这个条件呢?肯定是可以的,因为用户调用函数,不停地输入参数,arg.length 一直在增大,而fn.length 一直不变。整个curry 函数如下
let curry = function(fn) { if (typeof fn !== 'function') { throw new Error('只能对函数进行柯里化'); } return function curriedFn(...arg) { // 当调用返回的函数时,如果用户传递进来的参数多于我们要实行柯里化的函数参数,那直接让函数执行。 if (arg.length < fn.length) { return function(...otherArg) { // 递归调用curriedFn return curriedFn.apply(null, arg.concat(otherArg)); } } else { return fn.apply(null, arg); // 函数直接执行 } } }
现在写一个函数来体验一下这个柯里化函数。
// 函数接受四个参数 const multiply = (x, y, z, m) => x * y * z * m; let curryFn = curry(multiply); console.log(curryFn(1)(2)(3)(4));