函数柯里化为何物?

157 阅读5分钟

原文链接

定义

函数柯里化(Currying)是把接受多个参数的函数变成接受一个单一参数(最初的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

函数柯里化并不是JavaScript特有的。用笼统的话形容则是:减少函数参数个数的传递,并返回一个新的函数,这个新的函数能够处理旧函数剩下的参数。

简单示例:

// 计算两个数相加并返回计算结果的函数,接受两个参数a和b
function add (a, b) {
    return a + b;
}
// 将函数柯里化
function curry (a) {
    return function (b) {
        return a + b;
    }
}
// 应用函数柯里化
var _add = curry(1);
// 输出结果
console.log(_add(2)); // 3
// 比较结果
console.log(_add(2) === add(1, 2)); // true
// 另一种比较
console.log(curry(1)(2) === add(1, 2)); // true

这个是比较简单的函数柯里化过程,细心的同学会发现,示例中的函数封装(柯里化)方式是具有较大的局限性的,不过它能给大家对函数柯里化有一种大概的认识。

以上简单的示例可能并不能说什么,接下来,我们将给出更详细的例子去配合理解。

详细示例:

假设有一个接受三个参数且求三个数之和的add函数

function add (a, b, c) {
    // do something...
}

然后经过我们的柯里化(curry)函数封装后得到_add函数

var _add = curry(add);

那么_addcurry封装后返回的柯里化函数,根据上述的定义,它能够处理add的剩余参数。因此下面的函数调用都是等价的。

add(a, b, c) <=> _add(a, b, c) <=> _add(a, b)(c) <=> _add(a)(b,c) <=> _add(a)(b)(c)

所以说,柯里化也叫做"部分求值"。

实现

我们将上面简单示例中的curry函数,改成更加通用形式:

function curry(fn) {
    // 记录原函数参数个数
    var len= fn.length;
    // 记录传参个数
    var args = [].slice.call(arguments, 1);
    
    // 返回新的函数
    return function() {
        // 保存新接收的参数为数组
        var _args = [].slice.call(arguments);
        // 将新旧两参数数组合并
        [].unshift.apply(_args, args);
        
        // 如果累积接收的参数个数少于原函数接受的参数,则递归调用
        if (_args.length < len) {
            return curry.call(this, fn, ..._args);
        }
        // 若个数满足要求,则返回原函数调用结果
        return fn.apply(this, _args);
    }
}

示例应用:

function add (a, b, c) {
    console.log(a + b + c)
    return a + b + c;
}
var _add = curry(add);
_add(1, 2, 3);  // 6
_add(1)(2, 3);  // 6
_add(1, 2)(3);  // 6
_add(1)(2)(3);  // 6
var _add = curry(add, 1);
_add(2, 3);     // 6
_add(2)(3);     // 6
var _add = curry(add, 1, 2);
_add(3);        // 6
var _add = curry(add, 1, 2, 3);
_add();         // 6

这里代码逻辑也不难。我们只需判断参数个数是否达到函数柯里化前的个数,若没有,则递归调用柯里化函数;若达到了,则执行函数,并返回执行后的结果。

有的同学就苦恼了,函数柯里化,其实都最后还不是函数执行自身吗,为什么还搞那么多花里胡哨的骚操作呢?函数柯里化确实把问题复杂化了,但同时提高了函数调用的自由度,这正是函数柯里化的核心所在。

请看一个常见的例子。

假设我们有一个需求,需要验证用户输入是否是正确的手机号码,那么大家可能会这样封装函数:

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

又假设我们还有一个需求需要验证邮箱正确性,我们可能又有如下封装:

function checkEmail(email) {
    return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}

这时候,产品经理又过来问我们,能不能加上验证身份证号码、登陆密码之类的。因此,我们为了保持通用性,常常会有这样的封装:

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

这时,我们就会有这样子的调用形式:

check(/^1[34578]\d{9}$/, '12345678910');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'checkson@gmail.com');
...

如果要按照这种封装形式,我们要调用多次验证的话,需要多次传入相同的正则匹配,而正则匹配往往是固定不变的。那么这个时候,我们可以通过函数柯里化来让这些函数调用,变得优雅一些:

var _check = curry(check);

var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

最后的函数调用就会变得简洁明了了:

checkPhone('13912345678');
checkEmail('checkson@gmail.com');

我们可以发现,函数柯里化能够应对较复杂多变的业务需求,学好它是前端进阶的重点。

高级应用

前端有一道关于柯里化的面试题,广为流传。

题目: 实现一个add方法,使以下等式成立

add(1)(2)(3) = 6
add(1, 2)(3)(4, 5) = 15
add(1, 2, 3)(4, 5)(6) = 21
add(1, 2) = 3

这里需要补充一个重要的知识点:函数的隐式转换。当我们直接将函数参与其他运算的时候,函数会默认调用toString方法:

function fn () { return 1; }
console.log(fn + 1); // 输出结果为:function fn () { return 1; }1

我们可以重写函数的toString方法。

function fn() { return 1; }
fn.toString = function() { return 2; }
console.log(fn + 1); // 3

此外我们还可以重写函数的valueOf方法,得到同样的效果:

function fn() { return 1; }
fn.valueOf = function() { return 3; }

console.log(fn + 1); // 4

当同时重写函数的toStringvalueOf方法时,以valueOf为准。

function fn() { return 1; }
fn.toString = function() { return 2; }
fn.valueOf = function() { return 3; }

console.log(fn + 1); // 4

补充这个重要的知识点后,那么咱们直接撸代码了:

function add () {
  // 存储所有参数
  var args = [].slice.call(arguments);

  function adder () {
     // 保存参数 
    args.push(...arguments);
    // 重写valueOf方法
    adder.valueOf = function () {
      return args.reduce((a, b) => a + b);
    }
    // 递归返回adder函数
    return adder;
  }

  // 返回adder函数调用
  return adder();

}

代码校验:

console.log(add(1)(2)(3) == 6)             // true
console.log(add(1, 2)(3)(4, 5) == 15)      // true
console.log(add(1, 2, 3)(4, 5)(6) == 21)   // true
console.log(add(1, 2) == 3)                // true

这里代码的核心思想就是利用闭包来保存传入的所有参数和函数隐式转换。