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

栏目: JavaScript · 发布时间: 5年前

内容简介:这一节的内容,主要针对javascript函数式编程的两个重要概念,偏函数(partial application) 和函数柯里化(curry)进行介绍。着重讲解underscore中对于偏函数应用的实现。javascript的函数式编程有两个重要的概念,偏函数(partial application)和函数柯里化(curry)。理解这两个概念之前,我们需要先知晓什么是函数式编程? 函数式编程是一种编程风格,它可以将函数作为参数传递,并返回没有副作用的函数。而什么是偏函数应用(partial applica

这一节的内容,主要针对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中关于偏函数的应用,主要应用于延迟过程处理等。


以上所述就是小编给大家介绍的《打造属于自己的underscore系列(五)- 偏函数和函数柯里化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Inside Larry's and Sergey's Brain

Inside Larry's and Sergey's Brain

Richard Brandt / Portfolio / 17 Sep 2009 / USD 24.95

You’ve used their products. You’ve heard about their skyrocketing wealth and “don’t be evil” business motto. But how much do you really know about Google’s founders, Larry Page and Sergey Brin? Inside......一起来看看 《Inside Larry's and Sergey's Brain》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具