阅读 613

打造属于自己的underscore系列(五)- 偏函数和函数柯里化

这一节的内容,主要针对javascript函数式编程的两个重要概念,偏函数(partial application) 和函数柯里化(curry)进行介绍。着重讲解underscore中对于偏函数应用的实现。

四, 偏函数和函数柯里化

4.1 基本概念理解

javascript的函数式编程有两个重要的概念,偏函数(partial application)和函数柯里化(curry)。理解这两个概念之前,我们需要先知晓什么是函数式编程? 函数式编程是一种编程风格,它可以将函数作为参数传递,并返回没有副作用的函数。而什么是偏函数应用(partial application), 通俗点理解,固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数;函数柯里化(curry)的理解,可以概括为将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。举两个简单的例子方便大家理解并对比其本质区别。

// 偏函数
function add(a, b, c) {
    return a + b + c
}
var resultAdd = partial(add, 1); // partial 为偏函数原理实现
resultAdd(2, 3) // 将多参数的函数转化为接收剩余参数(n-1)的函数

// 函数柯里化
function add (a, b, c) {
    return a + b + c
}
var resultAdd = curry(add) //  curry 为柯里化实现
resultAdd(1)(2)(3)  // 将多参数的函数转化成接受单一参数的函数
复制代码

在underscore中只有对偏函数应用的实现,并没有函数柯里化的实现,因此本文只对underscore偏函数的实现做详细探讨,而柯里化实现只会在文末简单提及。(tips: lodash 有针对curry的函数实现)

4.2 rest参数

偏函数和柯里化的实现依赖于reset参数的概念,这是一个ES6的概念,rest参数(...rest)用于获取函数的多余参数,比如;

function add (a, ...values) { console.log(values) } // [2,4,6]
add(1, 2, 4, 6) //  获取除了第一个之后的剩余参数并以数组的形式返回。
复制代码

underscore中的restArguments方法,实现了与ES6中rest参数语法相似的功能,restArguments函数传递两个参数,function 和起始reset的位置,返回一个function的版本,该版本函数在调用时会接收来自起始rest位置后的所有参数,并收集到一个数组中。如果起始rest位置没有传递,则根据function本身的参数个数来确定。由于描述比较晦涩难懂,我们可以举一个具体的例子

var result = function (a, b, c) {
    console.log(a) // 3
    console.log(b) // 15
    console.log(c) // [2, 3, 2]
    return 'haha'
}
var raceResults = _.restArguments(result);
raceResults(3,15,2,3,2)
复制代码

result函数从接收三个参数,经过restArguments方法转换后,将接收的多余参数以数组的方式存储。当传递起始reset位置即startIndex时,实例如下:

var result = function (a, b, c) {
    console.log(a) // 3
    console.log(b) // [15, 2, 3, 2]
    console.log(c) // undefined
    return ''
}
var raceResults = _.restArguments(result, 1);
raceResults(3,15,2,3,2)
复制代码

startIndex 会指定原函数在何处将余下的参数转换成rest,例子中会在第一个参数之后将参数转成rest数组形式。因此有了这两种情景,我们可以实现一个简化版的restArguments方法,具体的思路可以参考代码注释

/**
 * 模仿es6 reset参数
 * fn  函数
 * [startIndex]: 接收参数的起始位置,如未传递,则为fn本身参数个数
 */
_.restArguments = function (fn, startIndex) {
    return function () {
        var l = startIndex == null ? fn.length - 1 : startIndex; // 如果没有传递startIndex,则rest数组的起始位置为参数倒数第二个
        l = l - fn.length < 0 ? l : 0; // 如果startIndex有传递值,但该值超过函数的参数个数,则默认将rest数组的起始位置设为第一个
        var arr = []
        var args = slice.call(arguments);
        for (var i = 0; i < l; i++) {
            arr.push(args[i]) // arr 存储startIndex前的参数
        }
        var restArgs = slice.call(arguments, l)
        arr.push(restArgs) // 将startIndex后的参数以数组的形式插入arr中,eg: arr = [1,3,4,[2,5,6]]
        return fn.apply(this, arr) //  调用时,fn参数参数形式已经转换成 1,3,4,[2,5,6]
    }
}
复制代码

restArgument实现rest参数的形式,本质上是改变参数的传递方式,函数调用时会将指定位置后的参数转化成数组形式的参数。

4.3 不绑定this指向的偏函数应用

在4.1的偏函数概念理解中,我们已经了解了偏函数的概念和使用形式,即将多参数的函数转化为接收剩余参数(n-1)的函数。在underscore中_.partial方法提供了对偏函数的实现。

// 使用
_.partial(function, *arguments)
// 举例
var subtract = function(a, b) { return b - a; };
sub5 = _.partial(subtract, 5);
sub5(20); // 15
// 可以传递_ 给arguments列表来指定一个不预先填充,但在调用时提供的参数
subFrom20 = _.partial(subtract, _, 5);
subFrom20(20); // -15
复制代码

有了restArguments的基础,实现一个partial函数便水到渠成。调用partial时,函数经过restArguments这层包装后,函数的剩余参数直接转成rest数组的形式,方便后续逻辑处理。

/**
 * 偏函数
 * 不指定执行上下文
 */
_.partial = _.restArguments(function (fn, reset) { //  将后续参数转化成rest数组形式
    return function () {
        var position = 0
        var placeholder = _.partial.placeholder; //  占位符,预先不填充,调用时填充
        var length = reset.length;
        var args = Array(length);
        for (var i = 0; i < length; i++) {
            args[i] = reset[i] === placeholder ? arguments[position++] : reset[i]; // 预先存储partial封装时传递的参数,当遇到占位符时,用partial处理后函数调用传递的参数代替。
        }
        while (position < arguments.length) {
            args.push(arguments[position++]) // 将partial处理后函数调用的参数和原存储参数合并。真正调用函数时传递执行。
        }
        return fn.apply(this, args)
    }
})

_.partial.placeholder = _;
复制代码

偏函数的思想,本质上可以这样理解,将参数保存起来,在调用函数时和调用传递参数合并,作为真正执行函数时的参数。

4.4 绑定this指向的偏函数应用

_.partial方法虽然实现了偏函数,但是当方法的调用需要结合上下文时,patial方法无法指定上下文,例如

var obj = {
    age: 1111,
    methods: function (name, time) {
        return name + '' + this.age + time 
    }
}

var sresult = _.partial(obj.methods, 3);
console.log(sresult(5)) // 3undefined5
复制代码

从偏函数的定义我们知道,原生javascript中,Function.prototype.bind()已经可以满足偏函数应用了

function add3(a, b, c) { return a+b+c; }  
add3(2,4,8);  // 14

var add6 = add3.bind(this, 2, 4);  
add6(8);  // 14  
复制代码

而在underscore同样封装了这样的方法,_.bind(function, object, *arguments) , 从bind函数的定义中可以知道,该方法将绑定函数 function 到对象 object 上, 也就是无论何时调用函数, 函数里的 this 都指向这个 object,并且可以填充函数所需要的参数。它是一个能结合上下文的偏函数应用,因此只需要修改partial的调用方式即可实现bind方法。

/**
 * bind
 * 偏函数指定this
 */
_.bind = _.restArguments(function (fn, obj, reset) {
    return function () {
        var position = 0
        var placeholder = _.partial.placeholder;
        var length = reset.length;
        var args = Array(length);
        for (var i = 0; i < length; i++) {
            args[i] = reset[i] === placeholder ? arguments[position++] : reset[i]
        }
        while (position < arguments.length) {
            args.push(arguments[position++])
        }
        return fn.apply(obj, args) // 指定obj为执行上下文
    }
})
复制代码
4.5 其他版本偏函数

至此,underscore中关于偏函数的实现已经介绍完毕,其设计思想是先将参数保存起来,在调用函数时和调用传递参数合并,作为真正执行函数时的参数执行函数。因此抛离underscore,我们可以用arguments和es6的rest参数的方式来实现偏函数,下面提供两个简易版本。

// arguments版本
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)))
    }
}
// es6 rest版本
function partial(fn, ...rest) {
    return (...args) => {
     return fn(...rest, ...args)   
    }
}
复制代码
4.6 函数柯里化

前文提到,underscore并没有关于函数柯里化的实现,只在它的相似库lodash才有对柯里化的实现。柯里化的思想是将一个多参数的函数拆分为接收单个参数的函数,接收单个参数的函数会返回另一个函数,直到接收完所有参数后才返回计算结果。因此,实现思路可以参考以下两种,es6版本和前者的实现思路相同。

// 完整版柯里化 ES3
function curry(fn) {
    if(fn.length < 2) return fn; // 当fn的参数只有一个或者更少时, 直接返回该函数并不需要柯里化。
    const generate = function(args, length) {
        return !length ? fn.apply(this, args) : function(arg) {
            return generate(args.concat(arg), length -1) // 循环递归调用,直到接收完所有参数(与函数参数个数一致), 将所有参数传递给fn进行调用。
        }
    }
    return generate([], fn.length)
}
// 完整版柯里化es6
function curryEs6(fn) {
    if(fn.length < 2) return fn
    const generate = (args, length) => !length ? fn(...args) : arg => generate([...args, arg], length - 1);
    return generate([], fn.length)
}
复制代码

柯里化的实现思路多样,且衍生变种内容较多,这里不一一阐述,有时间再另写一篇深入探讨。而关于偏函数的应用,会有专门一节来介绍underscore中关于偏函数的应用,主要应用于延迟过程处理等。




关注下面的标签,发现更多相似文章
评论