深入理解javascript系列(十七):函数柯里化

967 阅读4分钟

之前的系列,我们介绍了什么是高阶函数。所有以函数作为参数的函数,都可以叫作高阶函数。并且我们常常利用高阶函数来封装一些公共逻辑。

本次,我们要继续学习,继续记录,柯里化。柯里化,其实就是高阶函数的一种特殊用法。

柯里化是指这样一个函数(假设叫做createCurry),它接收函数A作为参数,运行后能够返回一个新的函数,并且这个新的函数能够处理函数A的剩余参数。

文字总是不那么好去理解,下面我们就通过例子来理解吧。

假设有一个接收三个参数的函数A。

function A(a, b, c) {
    // to do something
}

又假设我们有一个已经封装好了的柯里化通用函数createCurry。他接收bar作为参数,能够将A转化为柯里化函数,返回结果就是这个被转化之后的函数。

var _A = createCurry(A);

那么_A作为createCurry运行的返回函数,能够处理A的剩余参数。因此下面的运行结果都是等价的。

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

函数A被createCurry转化之后得到柯里化函数_A,_A能够处理A的所有剩余参数。因此柯里化也被称为部分求值。

在简单的场景下,我们可以不借助柯里化通用式来转化得到柯里化函数,仅凭借眼力自己封装。

例如,有一个简单的加法函数,它能够将自身的三个参数加起来并返回计算结果。

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

那么add函数的柯里化函数_add则可以写成:

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

因此下面的运算方式是等价的。

add(1, 2, 3);
_add(1)(2)(3);

当然,柯里化通用式具备更加强大的能力,仅靠眼力劲可不行。因此我们更需要知道如何封装这样一个柯里化的通用式。

首先通过_add可以看出,柯里化函数的运行过程其实是一个参数收集过程,我们将每一次传入的参数收集起来,并在最里层进行处理。因此在实现createCurry时,可以借助这个思路来进行封装。

代码如下:

// arity 用来标记剩余参数的个数
// args 用来收集参数

function createCurry(func, arity, args) {
    //第一次执行时,并不会传入arity,而是直接获取func参数的个数 func.length

    var arity = arity || func.length;
    
    //第一次执行也不会传入args,而是默认为空数组
    var args = args || [];
    
    var wrapper = function() {
        
        //将wrapper中的参数收集到args中
        var _args = [].slice.call(arguments);
        [].push.apply(args, _args);


        //如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if(_args.length < arity) {
            arity -= _args.length;
            return createCurry(func, arity, args);
        }

        //参数收集完毕,执行func
        return func.apply(func, args);
    }

    return wrapper;
}

是不是有些不太容易理解,所以要多阅读几次。这个createCurry的封装其实是借助了闭包和递归,实现一个参数收集,并在收集完毕之后执行所有参数。

不知道您是否有发现,函数经过createCurry转化为一个柯里化函数后,最后执行的结果,不是正相当于执行函数自己吗?柯里化是不是把简单的问题复杂化了?

没错,柯里化确实是把简单的问题复杂化了,但在复杂化的同时,我们在使用函数时拥有了更多的自由度。对于函数参数的自由处理,正是柯里化的核心所在。

下面举一个常见的例子。

如果想要验证一串数字是否是正确的手机号,那么按照正常思路来做,可能就会写出代码如下唉:

fuction checkPhone(phoneNumber) {
    return /^1[34578]\d{9}$/.test(phoneNumber);
}

而如果想要验证是否是邮箱呢?你然后在写一个,可是我们还会遇到更多需要验证的消息,如“身份证、登录名、密码...”。为了偷懒,我们应该封装一个更为通用的函数,把待验证的正则表达式与将要被验证的字符串作为参数传入:

function check(reg, targetString) {
    return reg.test(targetSting);
}

但是这样封装之后,在使用时又会遇到问题,因为总是需要输入一串正则,一串字符,这样就导致使用时效率低下。

这个时候,我们就可以借助柯里化,在check的基础上再做一层封装,以简化使用。

var _check = createCurry(check);

var checkPhone = _check(/xxxxxx/);
var checkEmail = _check(/xxxxxx/);

最后在使用时就会变得更加简洁与直观了。

checkPhone('13979227922');
checkEmail('xsxsx@163.com');

在这个过程中可以发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变时,柯里化依然能够应付自如。

虽然柯里化在一定程度上将问题复杂化,也让代码变得更加不容易理解,但是柯里化在面对复杂情况时的灵活性却让我们不得不爱。