Skip to content

实现Curry函数引发的思考 #6

@SoloJiang

Description

@SoloJiang

函数柯里化被提过很多次,简单说它的概念是:只传递给函数一部分参数去调用它,让它返回一个函数去处理剩下的参数。

函数柯里化被提过很多次,简单说它的概念是:只传递给函数一部分参数去调用它,让它返回一个函数去处理剩下的参数。

第一版参考代码

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就不用过多解释了,目的就是告诉接下来执行的curryfn还剩多少个参数没有传完。
如此递归下去,直到arguments.length<lengthfalse 时,说明参数已经接受完毕,然后执行 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请求为例,我们可以有效的从后端请求到的数据中,根据不同的输入情况得到我们最后想要的结果,具体应用方面的介绍,等我有空再写笔记吧~~~

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions