函数柯里化
发布于 4 年前 作者 junshen 2814 次浏览 来自 分享

定义

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
举个例子:

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

函数add只接受2个参数,curryAdd接受三个单个参数。我们将add(1,3)函数调用转换为curryAdd(1)(3)的多个函数调用。
所以__柯里化__就是把一个多参数函数转换为一系列只带单个参数的函数。

原理解析

通过递归来将 currying 的返回的函数也自动 Currying?化。从而形成了闭包,作用域可以访问上次传进来的参数,尽管这些函数很早就返回了,并且从内存进行了垃圾回收,但是它们的变量仍然保持alive。

function curry(fn, ...args){
    if(args.length >= fn.length){
    	return fn(...args);
    }
    
    return function(...args2){
    	return curry(fn, ...args, ...args2);
    }
}

上面函数是实现了currying化的核心代码,比较多次接受的参数总数与函数定义时的入参数量,当接受参数的数量大于或等于被 Currying 函数的传入参数数量时,就返回计算结果,否则返回一个继续接受参数的函数。

用途

编写小模板代码,参数复用,提高适用性



__举个例子__

你有一个商店,想给全部顾客10%的折扣优惠。

function discount(price, discount){
    return price * discount;
}

某日,有十个顾客光顾了你的商店,你给这十个顾客计算了十次同样10%的折扣优惠。

const price = discount(1500, 0.10);  // $150
// $1500 - $150 = $1350
const price = discount(2000, 0.10);  // $200
// $2000 - $200 = $1800
const price = discount(50, 0.10);  // $5
// $50 - $5 = $45
const price = discount(5000, 0.10);  // $500
// $5000 - $500 = $4500
const price = discount(300, 0.10);  // $30
// $300 - $150 = $270

这样无疑是增加了代码和计算的重复量。我们可以柯里化这个折扣函数,就不需要每天添加这 10%的折扣。

function discount(discount){
    return (price) => {
        return price * discount;
    }
}
const tenPercentDiscount = discount(0.1);

进行了柯里化处理这个函数后,我们每次只需要输入顾客购买的金额去进行计算价格就可以。

tenPercentDiscount(500);  // $50
// $500 - $50 = $450

如果此后你的打折力度加大,折扣优惠变成了20%,那么我们可以设置一个接受20%折扣的柯里化函数。

const twentyPercentDiscount = discount(0.2);
twentyPercentDiscount(500);  // $100
// $500 - $100 = $400

封装通用的柯里化函数

当我们只调用两次时,可以这样封装。

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

如果只调用三次:

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

上面的封装看上去跟我们想要的结果有点类似,但是参数的使用被限制得很死,因此并不是我们想要的最终结果,我们需要通用的封装。应该怎么办?总结一下上面的原理和例子,其实我们是利用闭包的特性,将所有的参数,集中到最后返回的函数里进行计算并返回结果。因此我们在封装时,主要的目的,就是将参数集中起来计算。

第一版
// 第一版
var curry = function(fn){
    var args = [].slice.call(arguments, 1);
    return function(){
    	var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    }
}

我们可以这样使用:

var addCurry = curry(add,1,2);
addCurry();  //3
// 或者
var addCurry = curry(add, 1);
addCurry(2);  //3
// 或者
var addCurry = curry(add);
addCurry(1,2);  //3

已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。

第二版
function curry(fn, length){
    length = length || fn.length;
    var slice = Array.prototype.slice;
    
    return function(){
    	if(arguments.length < length){
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        }else{
            return fn.apply(this, arguments);
        }
    }
}

我们验证下这个函数:

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

fn("a", "b", "c");   //["a","b", "c"]
fn("a", "b")("c");   //["a","b", "c"]
fn("a")("b")("c");   //["a","b", "c"]
fn("a")("b", "c");   //["a","b", "c"]
第三版

curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢?我们可以创建一个占位符,比如这样:

var fn = curry(function (a, b, c){
    console.log([a,b,c]);
})
fn("a", _, "c")("b");   //["a", "b", "c"];

第三版代码:

// 第三版
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);
        }
    }
}

var _ = {};

var 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);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)

结论

希望大家读完后能理解柯里化的概念,在实际开发中可以在合适的场景去使用,真正发挥他的价值。

回到顶部