# 壹.3.2 JavaScript函数柯里化

第一次看到柯里化这个词的时候，还是在2007年看一篇算法相关的博客提到把函数柯里化，那时一看这个词就感觉很高端，实际上了解了后才发现其实就是高阶函数的一个特殊用法。

## 柯里化的定义

> 柯里化（Currying）是一种技术，它把接受m个参数的函数变成接受n个参数的函数（0\<n\<m），并且该函数返回一个新函数，这个新函数接受余下的参数……如此循环，直到最后返回结果。

看这个定义可能有一点抽象，我们就来看一个简单的示例。

```javascript
// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3
```

实际上就是把add函数的x，y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。

那么，柯里化额外的封装了一层有什么具体的好处呢？

## 柯里化的好处

### **1. 复用参数**

```javascript
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false
```

上面的示例是一个正则的校验，正常来说直接调用check函数就可以了，但是如果我们有很多地方都要校验是否有数字，第一个参数其实没有变化，变化的是第二个参数。Currying之后，第一个参数reg的输入就可以省略掉，后面敲代码更省事了。

### **2. 延迟运行**

先仍然看一个add函数代码：

```javascript
// 利用reduce实现多参数版add
let add = function(...args){
    return args.reduce(function(accumulator, currentValue) {
        return accumulator + currentValue;
    },0)
};

// 一个简单的currying实现
function currying(func) {
    const args = [];
    return function result(...rest) {
        if (rest.length == 0) {
            return func(...args);
        } else {
            args.push(...rest);
            return result;
        }
    }
}

let sum = currying(add);

sum(1)(2)(3);   //未真正执行求和运算
sum(4);         //未真正执行求和运算
sum();   //执行求和
```

上面代码让函数通过柯里化，判断参数，如果有参数，就保存起来，不执行求和，直到最后一步才真正执行求和运算，达到了延迟运行的效果。

读者可能会问，延迟运行一定就要柯里化吗？当然不是。延迟运行和柯里化没有必然联系，本质上延迟运行，函数返回一个新函数（闭包）就可以实现了，这里只是举例说明柯里化有这个特性而已。

顺带提一下，代码中用到了ES6的[reduce](../../2.-he-xin-gai-nian/1.2.9#yi-2-97-reduce)和[rest运算符](../../2.-he-xin-gai-nian/1.2.12#rest)，对阅读理解可能有点影响，但熟悉语法运用之后，就会发现这些语法糖写出来的代码很简洁。

另外，js中经常使用的bind，实现的机制就是Currying。bind 用来改变函数执行时候的this，但是函数本身并不执行，所以是延迟运行，这一点和`call / apply`直接执行有所不同。

```javascript
Function.prototype.bind = function (context) {
    var _this = this;
    var args = Array.prototype.slice.call(arguments, 1);

    return function() {
        return _this.apply(context, args);
    }
}
```

### 3.参与科学计算

援引百度百科的解释：

> 在[理论计算机科学](https://baike.baidu.com/item/%E7%90%86%E8%AE%BA%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/11034581)中，柯里化提供了一个办法，可以在简单的理论模型中，比如只接受一个单一参数的lambda演算中研究带有多个参数的函数。

## 通用的封装

给定一个函数fn，设计一个通用封装（currying函数）作用于这个fn，让这个fn可以支持柯里化，该怎么实现呢？思路：

* 要让`fn(a,b)`等价于`fn(a)(b)`，那么`fn(a)`要返回一个函数，这个函数接受参数b，并返回与`fn(a,b)`相同的结果。
* 设计一个currying函数，它接受第一个参数是要被柯里化的函数fn，第2\~N个参数是原本要传递给fn的参数（这个可用rest操作符实现）。
* **柯里化主要围绕“处理参数”思考，不管怎么变化传参形式，柯里化之后的函数要把之前函数的参数都统统处理完毕才算合格。**

代码如下：

```javascript
// 初步的柯里化函数，其中fn是即将被柯里化的函数
var primaryCurrying = function (fn,...args) {
    return function (...args2) {
        // 将以后传给这个闭包函数里的全部参数和之前的args进行合并
        var newArgs = args.concat(args2);
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs);
    }
}

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

var add1= primaryCurrying(add, 1);
var result = add1(2);
console.log(result); //>> 3
```

看上面代码示例，已经有点柯里化了，但是还不符合要求。因为这个代码柯里化后得到的新函数`add1`只支持传“1段”参数，如果连续多段地传参数比如`curry(a)(b)`这样的话就不支持了。

{% hint style="info" %}
笔者注：函数右边带的一个可以处理参数的小括号对()，本书称&#x4E3A;**“1段”** 。函数若能多带1段，我们称函数有多1段的能力。
{% endhint %}

代码如下：

```javascript
var curry = primaryCurrying(add);

var add = curry(1,2);
console.log(add);//>> 3

var add = curry(1)(2);
console.log(add); //>> TypeError: curry(...) is not a function
```

看上面代码第7行报错部分，是因为`curry(1)`没有返回函数，怎么办呢？若参数没处理完，想办法返回函数就行了。一般这种情况可以用递归再封装一层。我们可以把这个`primaryCurrying`函数用作辅助函数（因为它已支持1段以便处理参数了），帮助我们写真正的 柯里化函数。

```javascript
// 借用之前的初步柯里化函数，让柯里化后的函数fn有多1段的能力。
var primaryCurrying = function (fn,...args) {
    return function (...args2) {
        var newArgs = args.concat(args2);
        return fn.apply(this, newArgs);
    }
}

/**
* 设计真正的柯里化函数。
* @param fn     即将被柯里化的函数。
* @param length 用来记录fn应该处理的剩余参数的长度。
*/
function curry(fn, length) {
    length = length || fn.length;

    return function (...args2) {
        //若原本要传给fn的参数还未传完
        if (args2.length < length) {
            //合并参数
            var combinedArgs = [fn].concat(args2);
            //递归，进一步柯里化。这里调用了primaryCurrying函数，每调用一次该函数，
            //就可以多“1段”以便可以处理掉剩余的参数，直到把所有应传给fn的参数都处理完。
            return curry(primaryCurrying.apply(this, combinedArgs), length - args2.length);
        } 
        //若原本要传给fn的参数都已经传完，则直接执行fn函数
        else {
            return fn.apply(this, args2);
        }
    };
}
```

这边其实是在初步的基础上，加上了递归的调用，只要原本要传给fn的参数还未传完，就会继续执行递归。来测试一下效果怎么样？

```javascript
var fn = curry(function (a, b, c) {
    return [a, b, c];
});

var l=console.log;
l(fn("a", "b", "c")); //>> ["a", "b", "c"]
l(fn("a", "b")("c")); //>> ["a", "b", "c"]
l(fn("a")("b")("c")); //>> ["a", "b", "c"]
l(fn("a")("b", "c")); //>> ["a", "b", "c"]
```

## 带占位符的封装

占位符可以支持不按顺序传递参数，举例要求如下：

```javascript
var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b"); //>> ["a", "b", "c"]
```

这里给出一个比较强大的代码实现：

```javascript
function curry(fn, args, holes) {
    let length = fn.length;//fn的形参的长度
    args = args || [];//fn的实参
    holes = holes || [];//占位符数组

    return function (...args2) {
        let _args = args.slice(0),//存放组合后的参数
            _holes = holes.slice(0),
            argsLen = args.length,//fn的实参的长度
            holesLen = holes.length,
            index = 0;

        for (let i = 0; i < args2.length; i++) {
            let arg = args2[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);
        }
    }
}

///////////////////////开始测试/////////////////////////

let _ = { '@@functional/placeholder':true};//定义一个占位符，这里参考Ramda的定义
let fn = curry(function (a, b, c, d, e) {
    console.log([a, b, c, d, e]);
});

// 验证 输出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
//上面3个很好理解，下面3个需要注意理解规则
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5);
```

## 柯里化性能

Currying的一些性能问题，笔者总结目前主要是以下四点：

* 一些实现基于存取arguments对象，通常要比存取命名参数慢一点；
* 一些老版本的浏览器在arguments.length的实现上是相当慢的；
* 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点；
* 创建大量嵌套作用域和闭包函数会带来花销，无论是在内存还是速度上。

其实在大部分应用中，主要的性能瓶颈是在操作DOM节点上。用JavaScript的性能损耗相对DOM操作的性能损耗而言，基本是可以忽略不计的，所以柯里化在大多数场合是可以放心使用。

## 一道经典面试题

```javascript
// 实现一个add方法，使计算结果能够满足如下预期：
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

////////////////////////////////////////
function add() {
    // 第一次执行时，定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数，利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性，当最后执行时隐式转换，并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (accumulator, currentValue) {
            return accumulator + currentValue;
        }, 0);
    }
    return _adder;
}
////////////////////////////////////////////////////////
add(1)(2)(3).toString();                //>> 6
add(1, 2, 3)(4).toString();             //>> 10
add(1)(2)(3)(4)(5).toString();          //>> 15
add(2, 6)(1).toString();                //>> 9
```

最后，柯里化是实现函数式编程的重要技巧之一，更多深入知识，可以查看[本书函数式编程](../../id-5/2.-han-shu-shi-bian-cheng/5.2.1)部分的内容。

{% hint style="info" %}
参考文献：\
[Favoring Curry](https://fr.umio.us/favoring-curry/)
{% endhint %}
