什么是函数科里化?

简单来说,就是能将多次传入参数的函数转换为单次传入参数函数的过程

举个栗子

function multiFn(a, b, c) {
    return a * b * c;
}

期望如下的输出,能得到上面这个函数一样的结果。

var newFun = currying(multiFn);
newFun(2)(3)(4);
newFun(2,3,4);
newFun(2)(3,4);
newFun(2,3)(4);

是不是跟bind函数的调用有点像。这个函数看着还是挺神奇的~~

显然,如果我们通过多次return函数的形式不能实现,因为这种方式是比较死板的。

函数科里化的两个最重要的特点就是:

  • 可以多次延迟调用
  • 传参比较灵活

核心步骤分析

从调用方式可以看出,函数什么时候执行,是跟原函数multiFn有密切联系的。实际上他们的关系是这样子的:

  • 各种花里胡哨传入参数的总长度小于multiFn的长度时,代表参数还未到位,需要使用闭包来继续收集函数参数
  • 当各种花里胡哨传入参数的总长度等于multiFn的长度时,代表参数到位了,可以直接执行函数了
则重点是需要明确(当前调用时的参数curArgs长度)+(之前传入的参数长度storeArgs)与 (未被科里化前的函数multiFn形参长度)的关系

接下来开始我们的代码实现部分。newFun(2,3)(4)的调用方式来说,首次过程分析如下:

  1. 先记录mutilFn形参个数,存储之前传入的参数,以便于之后的拼接及上述两者的参数长度比较。
 function currying(func, storeArgs) {
    var arity = func.length;    // 记录目标函数mutilFn形参个数
    var args = storeArgs || [];   // 记录之前传入的参数集合
}

2.通过调用的方式明确是要返回一个函数,并且这个函数会被递归调用的。在此之前,先明确一个知识点。

[].slice.call(arguments)是将类数组arguments转换为数组,apply和call传入的第一个参数为null时,实际上指向的是window
return function(){
      // 获取调用时传入的参数,将参数转化为数组。curArgs = [2,3]
      var curArgs = [].slice.call(arguments);
      
      // 将上次的参数与当前参数进行组合,并修正传参顺序。第一次时上一次存储的参数args为空,则拼接完curArgs为[2,3]
      Array.prototype.unshift.apply(curArgs, args);
      
      // 发现参数不够,返回闭包函数继续收集参数,并且需传回当前收集的参数curArgs.为了给调用的函数继续延迟调用,则需要返回函数而不是直接调用函数。
        if(curArgs.length < arity) {
            return currying.call(null, func, curArgs);
        }
        // 4、参数不够,不会被执行
        return func.apply(null, curArgs);
      
}

3.分析第二次参数传入的过程

 return function () {
        // 获取调用时传入的参数,将参数转化为数组。curArgs=[4]
        var curArgs = [].slice.call(arguments);
        
        // 将上次的参数与当前参数进行组合,并修正传参顺序。上一次参数为[2,3],则拼接完curArgs=[2,3,4]
        Array.prototype.unshift.apply(curArgs, args);
    
        // 3、参数足够,跳过
        if(curArgs.length < arity) {
            return currying.call(null, func, curArgs);
        }
        
        // 4、参数够了,则直接执行被转化的函数
        return func.apply(null, curArgs);
    } 

4.至此,按照ES5方式实现的代码完毕

 function currying(func, storeArgs) {
    var arity = func.length;
    var args = storeArgs || []; 
    return function () {
        var curArgs = [].slice.call(arguments);
        Array.prototype.unshift.apply(curArgs, args);
        if(curArgs.length < arity) {
            return currying.call(null, func, curArgs);
        }
        return func.apply(null, curArgs);
    }
  
}

根据如上的分析过程,可以采用es6实现参数默认值和参数转换为数组的这个步骤。可以得出ES6的实现如下:

function currying(func, storeArgs = []) {
    var arity = func.length;
    return function (...curArgs) {
        curArgs.unshift(...storeArgs);     // 将当前调用的参数融合进去既有的参数
        if (curArgs.length < arity) {      // 参数未到位,继续收集参数,并需将上次收集的参数继续传入
            return currying.call(null, func, curArgs);
        }   
        return func.apply(null, curArgs);  // 参数到位,直接执行函数
    }
}


function multiFn(a, b, c) {
    return  a *b *c;
}

var newFunc = currying(multiFn);
newFunc(1, 2)(3);
newFunc(1)(2, 3);
newFunc(1)(2)(3);
newFunc(1, 2, 3);

依据如上的思路,手写模拟实现bind函数。

bind函数的使用是延迟执行,第一个参数为调用对象,第二个参数为第一次传入的数值。整体思路为取出调用对象,取出所有应当被传入的数值参数,然后用apply来执行。
Function.prototype.bind = function () {
    var fn = this;           // 实际上就是bark函数
    var args = Array.prototype.slice.call(arguments);     // 将函数参数转化为数组。args有两个元素,第一个元素为调用对象,第二个参数为首次调用传入的数值
    var context = args.shift(); // context为弹出调用对象,此时args只剩一个形参,为数值
    return function () {
        return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
    }
}

// eg :
function bark(animal, behavior) {
    var str = `${animal} can ${behavior}`;
    return str;
}
var cat = {};
var fn = bark.bind(cat, 'Tom');
var result = fn('eat');
console.log(result);

小结

  1. 各种花里胡哨传入参数的总长度小于multiFn的长度时,代表参数还未到位,需要使用闭包递归调用来继续收集函数参数。
  2. 当各种花里胡哨传入参数的总长度等于multiFn的长度时,代表参数到位了,可以直接执行函数了。
参考链接:高阶函数应用 —— 柯里化与反柯里化

wuquan133
26 声望1 粉丝

正在成长中的小前端