Javascript中柯里化函数

165 阅读4分钟

javascript函数高级技巧中的柯里化是函数式编程的重要标志。它的特点是函数可以作为一个参数被传递,同时也可以作为一个函数的返回值被返回,在返回的闭包中再传入一些参数进行下一步的运算。先来一个简单的例子:

var sum = function(num1, num2){
    return num1 + num2;
}

var currying = function(num){
    return sum(5, num);
}

currying(3);    //8

这里定义了两个变量sum和currying,在currying中返回的是sum函数并传入了一个固定的参数5作为sum的第一个参数,再把调用currying传入的参数作为sum的第二个参数。这不是一个严格的柯里化函数,但可以清晰的展现柯里化的机制。对上面的例子进行改写,将函数作为sum的一个明显返回值。

var sum = function(num1){
    return function(num2){
        console.log(num1 + num2);
    }
}
    
sum3 = sum(3);
sum5 = sum(5);

sum3(5) // 8
sum5(5) // 10

sum3为一个闭包,其中有对之前传入的3的一个引用,调用sum3时将之前保存的3和后面传入的5一起给闭包使用,运算得到8,sum5与之类似。

通常的柯里化由以下步骤创建,调用一个函数并传入要柯里化的函数和参数。如下:

var currying = function(fn){
    // var args = Array.prototype.slice.call(arguments, 1); //下面的效果相同
    var args = [].slice.call(arguments, 1);
    return function(){
        var innerArgs = [].slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    }
}

var sum = function(num1, num2){
    return num1 + num2;
}

var curriedNum2 = currying(sum, 2);
curriedNum2(3);  // 5

var curriedNums = currying(sum, 2, 8);
curriedNums();  // 10

初次调用currying函数时传入sum函数和参数,返回一个闭包。在currying函数中将传入的参数存在args变量中,在闭包内将匿名函数的参数同args中的参数拼接,调用传入的sum函数并传给它拼接后的参数数组。

如果传入的参数未知,且约定规则在调用时,函数没有参数,执行的是“Getter”,而有参数的话,则是执行“Setter”。例子如下:

// 传入的参数为一个函数,并返回一个闭包函数
var currying = function(fn){
    var args = [];
    // return function(){
    return function res(){
        if(arguments.length === 0){
            return fn.apply(this,args);
        }else{
            // [].push.apply(args,arguments);
            Array.prototype.push.apply(args,arguments);
            
            // arguments.callee当前正在执行的函数,即currying的return值(一个闭包函数),此前的args,fn都得以保存,并能访问
            // 在ES6中不建议用arguments.callee,因此用一个具名函数代替
            // return arguments.callee;
            return res;
        }
    }
};

var count = function(...args){
    var sum = 0;
    for(item of args){
        sum += item;
    }
    return sum;
}

var calc = currying(count);

calc(100);
calc(200);
calc(300);
calc();         //  600


calc(100, 200, 300);
calc(400);
calc();         //  1600

第一次调用currying函数并传入count函数,在currying函数内返回一个闭包,闭包匿名函数的参数不为 0 时,将传入的参数添加到args数组中,并返回该匿名函数;为 0 时就调用fn函数并传给fn函数args参数。

在最后再放一个之前遇到的面试题。

题为:实现一个add方法,使计算结果能够满足如下预期:

add(1)(2)(3)() = 6;
add(1, 2, 3)(4)() = 10;
add(1)(2)(3)(4)(5)() = 15;

实现如下:

function add() {
    // 第一次执行时,定义一个数组用来存储所一次次传进来的参数
    var _args = [].slice.call(arguments); // Array.prototype.slice.call同效果

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

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

console.log(add(1)(2)(3)());   // 6
console.log(add(1, 2, 3)(4)()); // 10
console.log(add(1)(2)(3)(4)(5)()); // 15
console.log(add(2, 6)(1)());       // 9

另一个curry的封装方式sum求和函数的参数是固定的,参数个数倒计,把传进来的参数拼接起来,参数达到sum需要的个数时就执行sum,达不到就返回一个包含内部函数的包装函数,具体代码如下:

function sum(x, y, z, w) {
    return x + y + z + w
}
        
function curry(fn) {
    function inner(len, arg) {
        if (len === 0) {
            return fn.apply(null, arg)
        }
        return function(x) {
            return inner(len - 1, arg.concat(x))
        }
    }
    return inner(fn.length, [])
}
        
console.log(curry(sum)) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')('c')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')('c')('d')) // abcd
        
function curry2(fn) {
    function inner(len, arg) {
        if (len <= 0) {
            return fn.apply(null, arg)
        }
        return function() {
            return inner(len - arguments.length, arg.concat(Array.apply([], arguments)))
        }
    }
    return inner(fn.length, [])
}

console.log(curry2(sum)('A')('B', 'C')('D')) // ABCD
console.log(curry2(sum)('A')()('B', 'C')()('D')) // ABCD