# 函数柯里化

By [Murraya](https://paragraph.com/@murraya) · 2022-05-11

---

函数柯里化
=====

*   什么是函数柯里化？
    

什么是函数柯里化
--------

在计算机科学中，柯里化（英语：_Currying_），又译为卡瑞化或加里化，是把接受多个参数的函数变换成接受一个单一参数（最初函数的第一个参数）的函数，并且返回接受余下的参数而且返回结果的新函数的技术。

这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的，尽管它是 _Moses Schönfinkel_ 和戈特洛布·弗雷格发明的。

在直觉上，柯里化声称如果你固定某些参数，你将得到接受余下参数的一个函数。

我们姑且叫它返回函数，在调用返回函数的时候，它将判断当前的参数和之前被柯里化函数固定的参数拼起来之后，是否达到了原本函数的参数个数。

如果是，则执行原本的函数，得到结果；如果没有达到，则要继续调用柯里化函数来固定目前的参数。

在理论计算机科学中，柯里化提供了在简单的理论模型中，比如：只接受一个单一参数的 _lambda_ 演算中，研究带有多个参数的函数的方式。

函数柯里化的对偶是_Uncurrying_，一种使用匿名单参数函数来实现多参数函数的方法。

柯里化快速入门
-------

接下来，我们来通过一个简单的示例，让大家快速体会函数柯里化。

假设我们有一个求取两个数之和的函数：

    function add(x, y) {
        return x + y;
    }
    console.log(add(1, 2)); // 3
    console.log(add(5, 7)); // 12
    

在上面的示例中，我们有一个 _add_ 函数，接收两个形参，返回两形参的和。

在调用的时候，我们每次也需要传递两个参数。

现在，我们对其进行柯里化，如下：

    function add(x) {
        return function (y) {
            return x + y;
        }
    }
    console.log(add(1)(2)); // 3
    console.log(add(5)(7)); // 3
    

在上面的代码中，我们对 _add_ 函数进行了柯里化改造，只接受一个参数，但是返回的也不是值了，而是返回一个函数，这个函数也接收一个参数，然后利用闭包的特性，可以访问到最开始传入的 _x_ 的值，最终返回 _x_ 和 _y_ 的和。

所以，通过上面的这个示例，我们能够体会到前面所说的柯里化函数的特点：

一个柯里化的函数首先会接受一些参数，接受了这些参数之后，该函数并不会立即求值，而是继续返回另外一个函数，刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候，之前传入的所有参数都会被一次性用于求值。

函数柯里化实际应用
---------

通过上面的例子，我们体验到了什么是柯里化函数。

但是问题来了，费这么大劲封装一层，到底有什么用处呢？

没有好处想让我们程序员多干事情是不可能滴，这辈子都不可能。

所以接下来我们就来看一下函数柯里化的一个实际应用。

**参数复用**

就是将相同的参数固定下来。

    // 正常正则验证字符串 reg.test(txt)
    
    // 函数封装后
    function check(reg, txt) {
        return reg.test(txt)
    }
    
    // 即使是相同的正则表达式，也需要重新传递一次
    console.log(check(/\d+/g, 'test1')); // true
    console.log(check(/\d+/g, 'testtest')); // false
    console.log(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)
    
    console.log(hasNumber('test1')); // true
    console.log(hasNumber('testtest'));  // false
    console.log(hasLetter('21212')); // false
    

上面的示例是一个正则的校验，正常来说直接调用 _check_ 函数就可以了，但是如果我有很多地方都要校验是否有数字，其实就是需要将第一个参数 _reg_ 进行复用，这样别的地方就能够直接调用 _hasNumber、hasLetter_ 等函数，让参数能够复用，调用起来也更方便。

**提前确认**

    /**
     * 
     * @param {要绑定事件的 DOM 元素} element 
     * @param {绑定什么事件} event 
     * @param {事件处理函数} handler 
     */
    var on = function (element, event, handler) {
        if (document.addEventListener) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        } else {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        }
    }
    
    on(div, 'click', function(){})
    
    
    var on = (function () {
        if (document.addEventListener) {
            return function (element, event, handler) {
                if (element && event && handler) {
                    element.addEventListener(event, handler, false);
                }
            };
        } else {
            return function (element, event, handler) {
                if (element && event && handler) {
                    element.attachEvent('on' + event, handler);
                }
            };
        }
    })();
    
    on(div, 'click', function(){})
    
    //换一种写法可能比较好理解一点，上面就是把 isSupport 这个参数给先确定下来了
    var on = function (isSupport, element, event, handler) {
        isSupport = isSupport || document.addEventListener;
        if (isSupport) {
            return element.addEventListener(event, handler, false);
        } else {
            return element.attachEvent('on' + event, handler);
        }
    }
    on(true, div, 'click', function(){})
    on(true, div, 'click', function(){})
    on(true, div, 'click', function(){})
    

我们在做项目的过程中，封装一些 _DOM_ 操作可以说再常见不过，上面第一种写法也是比较常见，但是我们看看第二种写法，它相对于第一种写法就是自执行然后返回一个新的函数，这样其实就是提前确定了会走哪一个方法，避免每次都进行判断。

封装通用柯里化函数
---------

接下来我们来封装一个通用的柯里化函数。

    function curry() {
        var fn = arguments[0]; // 获取要执行的函数
        var args = [].slice.call(arguments, 1); // 获取传递的参数，构成一个参数数组
        // 如果传递的参数已经等于执行函数所需的参数数量
        if (args.length === fn.length) {
            return fn.apply(this, args)
        }
        // 参数不够向外界返回的函数
        function _curry(){
            // 推入之前判断
            // 将新接收到的参数推入到参数数组中
            args.push(...arguments);
            if(args.length === fn.length){
                return fn.apply(this, args)
            }
            return _curry;
        }
        return _curry;
    }
    

对上面的代码进行测试：

    // 测试 1
    function add(a, b, c) {
        return a + b + c;
    }
    
    console.log(curry(add)(1)(2)(3)); // 6
    console.log(curry(add, 1)(2)(3)); // 6
    console.log(curry(add, 1, 2, 3)); // 6
    console.log(curry(add, 1)(3, 4)); // 8
    
    var addCurrying = curry(add)(2);
    console.log(addCurrying(7)(8)); // 17
    
    // 测试 2
    function check(reg, txt) {
        return reg.test(txt)
    }
    var hasNumber = curry(check)(/\d+/g);
    console.log(hasNumber('test1'));// true
    

一道经典的柯里化面试题
-----------

实现一个 _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;
        };
    
        // 这个是最后输出的时候被调用的，return 后面如果是函数体，
        // 为了输出函数体字符串会自动调用 toString 方法
        // 利用 toString 隐式转换的特性，当最后执行时隐式转换，并计算最终的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }
    
        // 这个 return 是第一次调用的时候返回上面的函数体，
        // 这样后面所有的括号再执行的时候就是执行 _adder 函数体
        return _adder;
    }
    console.log(add(1)(2)(3).toString()); // 6
    console.log(add(1, 2, 3)(4).toString()); // 10
    console.log(add(1)(2)(3)(4)(5).toString()); // 15
    console.log(add(2, 6)(1).toString()); // 9

---

*Originally published on [Murraya](https://paragraph.com/@murraya/TIEeUJeSAUTisCGBQ2KI)*
