浅析函数柯里化与反柯里化

浅析函数柯里化与反柯里化

1. 柯里化

柯里化(Currying),又称部分求值(Partial Evaluation),可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。

1.1 作用

柯里化有3个常见作用:

  1. 参数复用
  2. 提前返回
  3. 延迟计算/运行

先看一个简单示例,实现 add(1)(2, 3)(4)() = 10 的效果:

let currying = (fn) =>  {
    let arr = [];
    // 相当于参数 fn 及 return 语句之前定义的变量都被保存下来了
    let next = (...args) => {
        if (args.length > 0) {
            arr.push(...args);
            // 实现链式调用
            return next;
        } else {
            return fn(...arr);
        }
    }
    return next;
    // ES5 写法
    // return function() {
    //     if (arguments.length === 0) {
    //         return fn.call(null, arr);
    //     } else {
    //         arr = arr.concat([].slice.call(arguments));
    //     }
    // }

}

let add = currying((...args) => {
    let sum = 0;
    for (let item of args) {
        sum += item;
    }
    return sum;
})

add(1);
add(2, 3);
add(4);
let sum = add(5)();
console.log(sum); // 15

从上例可以看出,柯里化有延迟计算/运行的作用,而且可以保存变量,因此,我们可以通过柯里化实现参数复用

// 监听事件
function nodeListen(node, eventName){
    return function(fn){
        node.addEventListener(eventName, function(){
            fn.apply(this, Array.prototype.slice.call(arguments));
        }, false);
    }
}

var bodyClickListen = nodeListen(document.body, 'click');
bodyClickListen(function(){
    console.log('first listen');
});

bodyClickListen(function(){
    console.log('second listen');
});

// 有点懒,就不搞成 ES6 的写法了

提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法,可以减少判断次数(参考张鑫旭的JS中的柯里化(currying)):

// 原先的写法
var addEvent = function(el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};

// 柯里化,只走一次判断
var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();

1.2 函数的执行时机

add(1)(2, 3)(4)(5) = 15 为例,如何知道柯里化函数的执行时机?

总结了3种方法:

  • 判断最后一次调用时传入的参数特征

    以上文中的将 add 函数柯里化的例子 ,判断传入参数的个数以结束函数调用,但这种方法不够优雅

  • 隐式类型转化:valueOftoString

    修改上文 add 函数柯里化的例子

    let currying = (fn) => {
        let arr = [];
        let next = (...args) => {
            arr.push(...args);
            return next;
        }
    
        // 字符类型
        next.toString = () => {
            console.log('toString')
            return fn(...arr);
        };
    
        // 数值类型
        next.valueOf = () => {
            console.log('valueOf')
            return fn(...arr);
        };
    
        return next;
    }
    
    let add = currying((...args) => {
        let sum = 0;
        for (let item of args) {
            sum += item;
        }
        return sum;
    })
    // 注意 ‘+’,这里进行了隐式类型转化
    let sum = +add(1)(2, 3)(4);
    console.log(sum);
    //valueOf
    //10
    

    如果我们没有重新定义 valueOftoString,其**隐式转换会调用默认的toString()**方法,将函数本身内容作为字符串返回;如果我们自己重新定义toString/valueOf方法,其中 valueOftoString 优先级更高

    可以参考 有趣的JavaScript隐式类型转换,简单了解一下隐式类型转换

  • 判断原函数(被柯里化函数的参数数量)

    主要用到了函数的 length 属性,示例参照下一节通用柯里化函数

1.3 通用柯里化函数

三行代码实现柯里化函数:

const curry = (fn) => {
    if (fn.length <= 1) return fn;

    const generator = (args) => (args.length === fn.length ? fn(...args) : arg => generator([...args, arg]));

    // 上述函数拆分后
    // const generator = (args) => {
    //     if (args.length === fn.length) {
    //         return fn(...args);
    //     } else {
    //         return arg => generator([...args, arg])
    //     }
    // }

    return generator([], fn.length);
};

const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
const res = curriedSum(1)(2)(3)
console.log(res); // 6

还有一个一行实现的:

const curry = (fn, arr = []) => (...args) => (
  arg => arg.length === fn.length
    ? fn(...arg)
    : curry(fn, arg)
)([...arr, ...args])

emmmmm,我的小脑筋不适合这个…

1.4 Function.prototype.bind 方法也是柯里化应用

与 call/apply 方法直接执行不同,bind 方法将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数(绑定函数), 这符合柯里化特点。

function addArguments(arg1, arg2) {
    return arg1 + arg2
}

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var result1 = addThirtySeven(5);
// 37 + 5 = 42 

var result2 = addThirtySeven(6, 10);
// 37 + 6 = 42 ,第二个参数被忽略

2. 反柯里化

反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。使本来只有特定对象才适用的方法,扩展到更多的对象。

即把如下给定的函数签名,

obj.func(arg1, arg2)

转化成一个函数形式,签名如下:

func(obj, arg1, arg2)

2.1 简单实现

Function.prototype.uncurrying = function() {
    var that = this;
    return function() {
        return Function.prototype.call.apply(that, arguments);
    }
};

function sayHi () {
    return "Hello " + this.value +" "+[].slice.call(arguments);
}
var sayHiuncurrying=sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));
  • uncurrying是定义在Functionprototype上的方法,因此对所有的函数都可以使用此方法。调用时候:sayHiuncurrying=sayHi.uncurrying(),所以uncurrying中的 this 指向的是 sayHi 函数; (一般原型方法中的 this 不是指向原型对象prototype,而是指向调用对象,在这里调用对象是另一个函数,在javascript中函数也是对象)
  • call.apply(that, arguments)that 设置为 call 方法的上下文,然后将 arguments 传给 call方法。上例中,that 实际指向 sayHi,所以调用 sayHiuncurrying(arg1, arg2, ...) 相当于 sayHi.call(arg1, arg2, ...)(apply的作用);
  • sayHi.call(arg1, arg2, ...), call 函数把 arg1 当做 sayHi的上下文,然后把 arg2,... 等剩下的参数传给 sayHi,因此最后相当于 arg1.sayHi(arg2,...)(call的作用);
  • 因此,这相当于 sayHiuncurrying(obj,args) 等于 obj.sayHi(args)

2.2 通用反柯里化函数

var uncurrying= function (fn) {
    return function () {        
        return Function.prototype.call.apply(fn,arguments);
    }
};

举例:

var $ = {};
console.log($.push);                          // undefined
var pushUncurrying = uncurrying(Array.prototype.push);
$.push = function (obj) {
    pushUncurrying(this,obj);
};
$.push('first');
console.log($.length);                        // 1
console.log($[0]);                            // first
console.log($.hasOwnProperty('length'));      // true

这里模仿了一个“类似jquery库” 实现时借用 Array 的 push 方法。 我们知道对象是没有 push 方法的,所以 console.log(obj.push) 返回 undefined,可以借用Array 来处理 push,由原生的数组方法(js引擎)来维护 伪数组对象的 length 属性和数组成员。

参考文献

JS中的柯里化(currying)

JavaScript 函数式编程技巧 - 柯里化

柯里化与反柯里化

三行代码实现 JS 柯里化

浅析 JavaScript 中的 函数 uncurrying 反柯里化

你可能感兴趣的:(JS基础)