一文搞懂函数柯理化

2,613 阅读3分钟

什么是函数的柯理化

柯里化(英语:Currying),又叫函数的部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

通俗一点来说

  • 柯里化就是一个函数原本有多个参数,只传入一个参数,生成一个新函数,由新函数接收剩下的参数来运行得到结果.
  • 与柯里化类似的一个概念:高阶函数(可以参考我上一篇文章什么是高阶函数

JavaScript高级程序设计里写到

  • 柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要的参数.

  • 高级程序设计上的通用例子:

function curry(fn) {
    // 获取剩余参数 将类数组转化成数组
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.contact(innerArgs);
        return fn.apply(null, finalArgs);
    }
}
function add (num1, num2) {
    return num1 + num2;
}
var curriedAdd = curry(add, 5);
alert(curriedAdd(3); // 8

这个一上来就给出结果可能不好理解,下面我们一步一步来,讲解柯理化的实现步骤及它解决了什么问题

我们目前有一个函数A

function A(a, b, c) {
   return a + b + c;
}

假如我们已经有一个封装好的柯理化函数 curry 他接收 A 作为参数,能够将 A 转化成柯理化函数,返回的结果就是这个被转化后的函数

var _A = currying(A);

那么 _A 作为柯理化函数,他能够处理 A 剩下的剩余参数,因此要求下面的运行结果都是等价的:

_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);

在简单的场景下,我们凭眼里来封装得到柯理化函数可能是下面这样的

function myA(a) {
    return function(b) {
      return function(c) {
        return a + b + c;
      }
    }
}

那么下面的运算方式确实是等价的

A(1, 2, 3);
myA(1)(2)(3);

但是按照要求,要求 myA(1, 2)(3) 也要与其他的等价,但并不是,所以,这样封装的柯理化函数自由度偏低,而且有些要求达不到

因此我们需要知道如何去封装一个柯理化的通用式

封装如下:

function curry(fn, args) {
    args = args || []
    var arity = fn.length
    
    return function() {
        var _args = Array.prototype.slice.call(arguments)
        Array.prototype.unshift.call(_args, ...args)
        _args = _args.concat(args)
        
        if (_args.length < arity) {
            return currying.call(null, fn, _args)
        }
        
        return fn.apply(null, _args)
    }
}

遇到的坑(重要)

在编写函数柯理化的时候遇到了一些坑,虽然不大,但是没注意的话还是会掉下去的,既然我掉下去了,那么就记录下来防止大家掉下去吧

函数的长度:Function.length

问问大家,知道函数的长度吗?

什么?函数还有长度?不就字符串,数组有长度吗?

是的,函数有长度,函数的长度就表示 该函数有多少个必须要传入的参数,即形参的个数

在《Javascript权威指南》中这样定义:
参数有形参(parameter)和实参(argument)的区别,形参相当于函数中定义的变量,实参是在运行时的函数调用时传入的参数。
说明白就是,形参就是函数声明时的变量,实参是我们调用该函数时传入的具体参数。

那我们先来看一个例子

var fn = function(a, b) {}
console.log(fn.length);

不知道函数的长度的概念的人可能不知道这题的答案,但是根据上面的说法,不是有两个形参吗,那么好,答案不就是 2 吗,好那看下一题

var fn = function(a, b = []) {}
console.log(fn.length);

这个打印多少呢?有的人应该就有点蒙了,应该还是 2 吧,我之前也这样以为,但正确答案却是 1 ,那么这是为什么呢

刚才说了,函数的长度表示该函数有多少个必须要传入的参数,即形参的个数,但是:形参数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数,与之对应的是:arguments.length 是函数被调用是实际传参的个数

好的,知道结论后大家可以将下面的代码拷贝到浏览器控制台粘贴看看答案,加深一下印象

function fn1(a = () => {}, b = [], c) {}
function fn2(a, b = [], c) {}
function fn3(...args) {}
function fn4() { alert(arguments.length) }

console.log(fn1.length); // 0
console.log(fn2.length); // 1
console.log(fn3.length); // 0
console.log(fn4.length); // 0