打造属于自己的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系列(五)- 偏函数和函数柯里化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 学习underscore源码整体架构,打造属于自己的函数式编程类库
- 打造属于你自己的instagram!
- 属于 Unity 的 Flutter——UIWidgets
- 利用Python写属于自己的翻译命令行
- 打造属于自己的underscore系列(六)- 洗牌算法
- 你属于程序员中的哪种人?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法导论(原书第2版)
[美] Thomas H.Cormen、Charles E.Leiserson、Ronald L.Rivest、Clifford Stein / 潘金贵 等 / 机械工业出版社 / 2006-9 / 85.00元
这本书深入浅出,全面地介绍了计算机算法。对每一个算法的分析既易于理解又十分有趣,并保持了数学严谨性。本书的设计目标全面,适用于多种用途。涵盖的内容有:算法在计算中的作用,概率分析和随机算法的介绍。书中专门讨论了线性规划,介绍了动态规划的两个应用,随机化和线性规划技术的近似算法等,还有有关递归求解、快速排序中用到的划分方法与期望线性时间顺序统计算法,以及对贪心算法元素的讨论。此书还介绍了对强连通子图......一起来看看 《算法导论(原书第2版)》 这本书的介绍吧!