前言

柯里化(currying)就是将使用多个参数的函数转换成一系列使用部分参数的函数的技术。
使用柯里化(currying)使我们把注意力集中于函数本身,而不必在意冗长的参数个数,使执行函数的代码更简洁,写出Pointfree的程序。
柯里化是JavaScript函数式编程的重点,其中应用函数闭包call和apply高阶函数递归等知识点,所以也是Javascript中的难点。
本文整理了JavaScript柯里化的基本概念,实现和应用场景。抛砖引玉一下,帮助读者掌握函数柯里化的基本知识点,能够在实际的开发中应用起来。
原创不易,您的点赞是我继续写作的动力,如果文章中有纰漏和错误,还望指出,谢谢!

概述

首先我们具象一下柯里化的概念。假设有一个接收3个参数的函数A

  1. function A(a,b,c){
  2. //todo something
  3. }

如果我们使用一个柯里化转换函数curry,这个函数接受函数作为参数,并返回函数

  1. const _A = curry(A);

函数_A可以接受1个或者多个参数,当总计传入的参数等于函数定义的参数个数时,输出结果。如下所示:

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

上述结果一次或者多次调用函数_A都返回相同的结果。
那么我们将curry称为对函数A的柯里化。因为curry接受一个函数并返回一个函数,curry又称为高阶函数
先撇开curry,我们先对一个简单的函数进行柯里化,如下:

function add(a,b){
  return a+b;
}
console.log(add(1+2)); //输出3
function _add(a){
    return function(b){
      return a+b;
  }
}
console.log(_add(1)(2));//输出3
console.log(_add(2)(1));//输出3

上述 _add是对 add的柯里化。然而

  1. 对于参数个数少的函数,柯里化相对简单,但是一旦参数增多,手动去柯里化不太现实;
  2. 我们也需要一个工具函数curry,去柯里化我们任意一个函数,用户只需要专注函数业务的实现
  3. 上述柯里化之后的参数顺序不一定可变,例如减法subtraction(10,1),上述柯里化之后只能subtraction(10)(1)不能subtraction(_,1)(10)

实现

上述阐述了柯里化的概念和实现效果。本节我们来实现工具函数curry
在开始之前,我们需要了解柯里化的思想。在概述中提过

总计传入的参数等于函数定义的参数个数时,输出结果

故柯里化思想就是一个积累函数参数,当参数个数一旦达到函数执行要求,执行函数,返回结果的过程。
JavaScript进阶之函数柯里化 - 图1

积累参数的过程,正如大坝后面的水库,积累参数的过程称为闭包,后面的水库就是内存

JavaScript进阶之函数柯里化 - 图2

参数一旦到达要求,返回结果,大坝一泻千里

于是我们的实现函数如下:

function curry(fn,...args){
  let argsLength = fn.length; //函数定义的形参个数
  return function() {
    //将上一次调用函数的参数和本次的参数合并
    var newArgs = args.concat([].slice.call(arguments)); 
    if(newArgs.length >= argsLength){
      return fn.apply(this,newArgs); //如果参数和执行的函数相等,执行函数
    }
    return curry.call(this,fn,...newArgs); //否则递归调用
  }
}

验证一下:

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

效果如下:

JavaScript进阶之函数柯里化 - 图3

下面是柯里化的骚气实现,对于ES6想深入的童鞋,可以好好看一下

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length? fn(...args): (...arg) => judge(...args, ...arg)

我把它转化成ES5,帮助理解。

var curry = function (fn){
   let judge = function(...args){
       if(args.length === fn.length){
      return  fn(...args)           
       }else{
           return function (...arg){
               return judge(...args, ...arg);
           }
       }
   }
   return judge;
}

引申

在上述实现的curry还是存在缺点,即柯里化之后的函数,只支持参数的顺序调用,如果要支持乱序,实现方式如下:

function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 处理类似 fn(1)(_) 这种情况
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 处理类似 fn(_, 2)(1) 这种情况
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用参数 1 替换占位符
                else {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

应用

柯里化的作用包括提高函数参数复用,延迟计算等,一般有如下几种应用:

偏函数

偏函数(Partial function),在python中应用较多,详情可查看这里,在Javascript也可以应用,如有一个int函数,如下:

function int(chars,hex=10){
        //将字符串chars转换成以hex进制的整数
}
int('10') //将10转换成10进制
int('10',2)//将10转换成2进制
int('10',8)//将10转换成8进制

该函数可以将默认的数字字符串转化成10进制整数,也可以指定hex的值。此处我们可以引申的柯里化函数,如下

let int2 = createCurrying(int,_,2);
int2('10');
let int8 = createCurrying(int,_,8);
int8('10');

简化回调

var persons = [{name: 'kevin', age: 11}, {name: 'daisy', age: 24}]

let getProp = createCurrying(function (key, obj) {
    return obj[key]
});
let names2 = persons.map(getProp('name'))
console.log(names2); //['kevin', 'daisy']

let ages2 = persons.map(getProp('age'))
console.log(ages2); //[11,24]

上述getProp经过柯里化,可以提升函数的复用性

延迟执行

原生事件监听的方法在现代浏览器和IE浏览器会有兼容问题,解决该兼容性问题的方法是进行一层封装,若不考虑柯里化函数,我们正常情况下会像下面这样进行封装,如下:

/*
* @param    ele        Object      DOM元素对象
* @param    type       String      事件类型
* @param    fn         Function    事件处理函数
* @param    isCapture  Boolean     是否捕获
*/
var addEvent = function(ele, type, fn, isCapture) {
    if(window.addEventListener) {
        ele.addEventListener(type, fn, isCapture)
    } else if(window.attachEvent) {

        ele.attachEvent("on" + type, fn)
    }
}
addEvent(document.getElementById('button'), "click", function() {
            alert("function currying");
 }, false)

柯里化之后,如下:

var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();
addEvent(document.getElementById('button'), "click", function() {
            alert("function currying");
 }, false)

此处使用柯里化延迟执行的特点,返回新函数,在新函数调用兼容的事件方法。等待addEvent新函数调用,延迟执行。

参考文献

场景去理解函数柯里化》
《JavaScript专题之函数柯里化》
JS中的柯里化(currying)
《前端基础进阶(八):深入详解函数的柯里化》
《掌握JavaScript函数的柯里化》