浅析函数柯里化

·  阅读 786
浅析函数柯里化

这是我参与更文挑战的第24天,活动详情查看: 更文挑战

函数柯里化是前端面试过程中场景的一种题目,它不算上高深莫测,但是又混合了多个JS语法,属于高阶函数中一种特殊的用法。

前言

维基百科:在计算机科学中,柯里化(Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

科里化面试题

在前端面试中有一个关于柯里化的面试题,流传甚广。

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
复制代码

很明显,计算结果正是所有参数的和,add方法每运行一次,肯定返回了一个同样的函数,继续计算剩下的参数。

1. 如果只调用2次

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

console.log(add(1)(2));  // 3
复制代码

2. 如果只调用3次

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

console.log(add(1)(2)(3)); // 6
复制代码

3. 如果调用的次数不确定?

其实上面的做法都是利用闭包的特性,将所有的参数,集中到最后返回的函数里进行计算并返回结果。因此我们在封装时,主要的目的,就是将参数集中起来计算。可以这样写个小结:

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

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

        // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }

        return _adder;
    }
    return adder.apply(null, [].slice.call(arguments));
}

// 输出结果,可自由组合的参数
console.log(add(1, 2, 3, 4, 5));  // 15
console.log(add(1, 2, 3, 4)(5));  // 15
console.log(add(1)(2)(3)(4)(5));  // 15
复制代码

利用闭包的特性通过一些方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。因此我们在调用add方法的时候,参数就显得非常灵活。

函数科里化

函数柯里化就是创建已经设置单个参数或者多个参数的函数,函数变为接受一个参数,返回一个值,柯里化的用途主要是参数复用。再举一个 demo :

// 参数复用
function add(a, b) {
    return a + b;
}
add(1,2)  //3

// 柯里化之后可以这样
var addCurry = curry(add);
addCurry(1)(2); //3
复制代码

或许针对这种简单的将两个数相加的场景,柯里化显得有点多余。但是如果我们想使用这个函数完成通用的事情,比如为所有的数加5,就可以使用addCurry(5)(x),使得将两个数相加的函数有了通用性。

通用版

var curry = function(func){

    var args = [].slice.call(arguments,1);

    return function(){

        var newArgs = args.concat([].slice.call(arguments));

        return func.apply(this,newArgs);

    }

}
复制代码

首先将参数进行分割,也就是将除了func之外的参数存进args。返回的函数接受新传入的参数并与之前的参数合并,从而将所有的参数传入函数中,并执行真正的函数。

改进版

比如说add这个函数接受两个参数,那么针对柯里化之后的函数,若传入的参数没有到达两个的话,就继续调用curry,继续接受参数。若参数到达2个了,就直接调用add函数。

var curry = function(func,args){
    var length = func.length;
    args = args||[];

    return function(){
        newArgs = args.concat([].slice.call(arguments));
        if (newArgs.length < length) {
            return curry.call(this,func,newArgs);
        } else {
            return func.apply(this,newArgs);
        }
    }
}

var addCurry = curry(add);
addCurry(1,2) //3
addCurry(1)(2) //3
复制代码

进阶版

但这一版柯里化函数仍然不能完全满足要求,因为它只针对有特定参数个数的函数适用。再回到 前面的那道面试题,题目要求不变。

add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
复制代码

前面写的已经不能满足需求,这里我们改用函数的 toString 来完成。

当我们返回函数的时候,会调用函数的toString来完成隐式转换,这样输出的就不是函数的字符串形式而是我们定义的toString返回的值。这样就既可以保持返回一个函数,又能够得到一个特定的值。

function add(){
    // 第一次执行时,定义一个数组专门用来储存所有的参数
    var args = [].slice.call(arguments);
    // 在内部声明一个函数,
    // 利用闭包的特性保存_args并收集所有的参数值
    var fn = function(){
        var newArgs = args.concat([].slice.call(arguments));
        return add.apply(null,newArgs);
    } 
    // 利用隐式转换的特性,
    // 当最后执行时隐式转换,并计算最终的值返回
    fn.toString = function(){
        return args.reduce(function(a, b) {
            return a + b;
        })
    }
    return fn ;
}

// 可以接受任意个数的参数
add(1)(2,3) //6
add(1)(2)(3)(4)(5) //15
复制代码

真正的函数柯里化

无论是上面的面试题还是demo,我们从中不难看出,柯里化是将接受多个参数转换为接受一个单一参数。简单梳理一下:

假设你有一个储钱罐 countMoney 函数,和一个记录本 arr 数组,当你每月有空钱时进行储存,每次在 arr 中记录一次,存入储钱罐中:

var arr=[];
var countMoney=function(arr){
    var sum=0;
    for(var i=0;i<arr.length;i++){
        sum+=arr[i];
    }
    return sum;
}

arr.push(1);
arr.push(2);

countMoney(arr);
复制代码

可以通过这种方式来进行存储,但是有本记录,是会被发现的,所以这个时候可以这样:

// 每次存储是调用一次,不需要再次记录下来
countMoney(1);
countMoney(2);

// 等到真正需要的时候我们可以直接计算出来这个总值
countMoney(); //3
复制代码

于是问题解决的方式变为柯里化问题,需要将多个参数接受转换为接受单一参数的问题。于是我们可以使用下面的方式进行处理。

var countMoney = (function() {
    let moneys = 0;
    let arr = [];

    var result = function() {
        // 判断是否还有参数,如果没有,则返回存储起来值的总和
        if(arguments.length == 0) {
            for(var i = 0; i < arr.length; i++) {
                money += arr[i];
            }
            return money;
        } else {
            // arguments 是个类数组来着,应该用展开符展开才能push进去
            // 通过arguments 处理可以传入多个参数值
            console.log(...arguments)
            arr.push(...arguments);
            return result;
        }
    }
    return result;
})();

countMoney(1)(2)(3)
countMoney(6)
复制代码

上面的例子完全可以实现柯里化,并且进行扩展,现在可以安全的存放钱了。

总结

柯里化(Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。

接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。将部分参数通过回调函数等方式传入函数中返回一个新函数,用于处理所有的想要传入的参数。

实际上,在JavaScript的很多思想和设计模式中,闭包是个很常见的且很重要的东西,上面两个例子代码中,本质上就是利用了闭包。上面的 countMoney 函数是个立即执行的函数,返回一个新函数,而这个新函数实际上就是一个闭包,这个新函数把每次接收到的参数都存储起来,并且继续返回一个新函数,当发现某次调用时没有传入参数,那就意味着要进行数据统计,从而把之前存储的数据一次拿出来计算,最后返回计算结果。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改