高级函数技巧-函数柯里化
栏目: JavaScript · 发布时间: 6年前
内容简介:我们经常说在Javascript语言中,如果你对函数式编程有一定了解,柯里化(Currying),维基百科上的解释是,把接受多个参数的函数转换成接受一个单一参数的函数
我们经常说在Javascript语言中, 函数
是“一等公民”,它们本质上是十分简单和过程化的。可以利用函数,进行一些简单的数据处理, return
结果,或者有一些额外的功能,需要通过使用闭包来实现,最后经常会 return
匿名函数。
如果你对函数式编程有一定了解, 函数柯里化 (function currying)是不可或缺的,利用函数柯里化,可以在开发中非常优雅的处理复杂逻辑。
函数柯里化
柯里化(Currying),维基百科上的解释是,把接受多个参数的函数转换成接受一个单一参数的函数
先看一个简单例子
// 柯里化
var foo = function(x) {
return function(y) {
return x + y
}
}
foo(3)(4) // 7
// 普通方法
var add = function(x, y) {
return x + y;
}
add(3, 4) //7
本来应该一次传入两个参数的add函数,柯里化方法,变成每次调用都只用传入一个参数,调用两次后,得到最后的结果。
再看看,一道经典的面试题。
编写一个sum函数,实现如下功能: console.log(sum(1)(2)(3)) // 6.
直接套用上面柯里化函数,多加一层 return
function sum(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
当然,柯里化不是为了解决面试题,它是应函数式编程而生,
如何实现
还是看看上面的经典面试题。
如果想实现 sum(1)(2)(3)(4)(5)...(n)
就得嵌套 n-1
个匿名函数,
function sum(a) {
return function(b) {
...
return function(n) {
}
}
}
看起来并不优雅,如果我们预先知道有多少个参数要传入,可以利用递归方法解决
var add = function(num1, num2) {
return num1 + num2;
}
// 假设 sum 函数调用时,传入参数都是标准的数字
function curry(add, n) {
var count = 0,
arr = [];
return function reply(arg) {
arr.push(arg);
if ( ++count >= n) {
//这里也可以在外面定义变量,保存每次计算后结果
return arr.reduce(function(p, c) {
return p = add(p, c);
}, 0)
} else {
return reply;
}
}
}
var sum = curry(add, 4);
sum(4)(3)(2)(1) // 10
如果调用次数多于约定数量, sum
就会报错,我们就可以设计成类似这样
sum(1)(2)(3)(4)(); // 最后传入空参数,标识调用结束,
只需要简单修改下 curry
函数
function curry(add) {
var arr = [];
return function reply() {
var arg = Array.prototype.slice.call(arguments);
arr = arr.concat(arg);
if (arg.length === 0) { // 递归结束条件,修改为 传入空参数
return arr.reduce(function(p, c) {
return p = add(p, c);
}, 0)
} else {
return reply;
}
}
}
console.log(sum(4)(3)(2)(1)(5)()) // 15
简洁版实现
上面针对具体问题,引入柯里化方法解答,回到如何实现创建柯里化函数的通用方法。
同样先看简单版本的方法,以 add
方法为例,代码来自《JavaScript高级程序设计》
function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
};
}
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add, 5);
var curriedAdd2 = curry(add, 5, 12);
alert(curriedAdd(3)) // 8
alert(curriedAdd2()) // 17
加强版实现
上面add函数,可以换成任何其他函数,经过curry函数处理,都可以转成柯里化函数。
这里在调用curry初始化时,就传入了一个参数,而且返回的函数 curriedAdd
, curriedAdd2
也没有被柯里化。要想实现更加通用的方法,在柯里化函数真正调用时,再传参数,
function curry(fn) {
...
}
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add);
curriedAdd(3)(4) // 7
每次调用 curry
返回的函数,也被柯里化,可以继续传入一个或多个参数进行调用,
跟上面 sum(1)(2)(3)(4)
非常类似,利用递归就可以实现。 关键是递归的出口,这里不能是传入一个空参数的调用, 而是原函数定义时,参数的总个数,柯里化函数调用时,满足了原函数的总个数,就返回计算结果,否则,继续返回柯里化函数
。
原函数的入参总个数,可以利用 length
属性获得
function add(num1, num2) {
return num1 + num2;
}
add.length // 2
结合上面的代码,
var curry = function(f) {
var len = f.length;
return function t() {
var innerLength = arguments.length,
args = Array.prototype.slice.call(arguments);
if (innerLength >= len) { // 递归出口,f.length
return f.apply(undefined, args)
} else {
return function() {
var innerArgs = Array.prototype.slice.call(arguments),
allArgs = args.concat(innerArgs);
return t.apply(undefined, allArgs)
}
}
}
}
// 测试一下
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add);
add(2)(3); //5
// 一个参数
function identity(value) {
return value;
}
var curriedIdentify = curry(identify);
curriedIdentify(4) // 4
到此,柯里化通用函数可以满足大部分需求了。
在使用 apply 递归调用的时候,默认传入 undefined, 在其它场景下,可能需要传入 context, 绑定指定环境
实际开发,推荐使用 lodash.curry , 具体实现,可以参考下 curry源码
使用场景
讲了这么多curry函数的不同实现方法,那么实现了通用方法后,在那些场景下可以使用,或者说使用柯里化函数是否可以真实的提高代码质量,下面总结一下使用场景
-
参数复用
在《JavaScript高级程序设计》中简单版的curry函数中
var curriedAdd = curry(add, 5)
在后面,使用curriedAdd函数时,默认都复用了
5,不需要重新传入两个参数 -
延迟执行
上面传入多个参数的
sum(1)(2)(3),就是延迟执行的最后例子,传入参数个数没有满足原函数入参个数,都不会立即返回结果。类似的场景,还有绑定事件回调,更使用bind()方法绑定上下文,传入参数类似,
addEventListener('click', hander.bind(this, arg1,arg2...)) addEventListener('click', curry(hander))延迟执行的特性,可以避免在执行函数外面,包裹一层匿名函数,curry函数作为回调函数就有很大优势。
-
函数式编程中,作为compose, functor, monad 等实现的基础
有人说柯里化是应函数式编程而生,它在里面出现的概率就非常大了,在 JS 函数式编程指南 中,开篇就介绍了柯里化的重要性。
关于额外开销
函数柯里化可以用来构建复杂的算法 和 功能, 但是 滥用 也会带来额外的开销。
从上面实现部分的代码中,可以看到,使用柯里化函数,离不开闭包, arguments, 递归。
闭包,函数中的变量都保存在内存中,内存消耗大,有可能导致内存泄漏。
递归,效率非常差,
arguments, 变量存取慢,访问性很差,
参考链接
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Python 工匠:让函数返回结果的技巧
- 用于简化和改进代码的函数式编程技巧
- Python 技巧 | 让函数返回结果的7个建议
- Less编写函数(mixin/@functions)的小技巧分享
- 分享几个Python小技巧函数里的4个小花招
- ES6小技巧 - 使用解构赋值设置函数参数默认值
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图解Java多线程设计模式
[日] 结城浩 / 侯振龙、杨文轩 / 人民邮电出版社 / 2017-8 / 89.00元
本书通过具体的Java 程序,以浅显易懂的语言逐一说明了多线程和并发处理中常用的12 种设计模式。内容涉及线程的基础知识、线程的启动与终止、线程间的互斥处理与协作、线程的有效应用、线程的数量管理以及性能优化的注意事项等。此外,还介绍了一些多线程编程时容易出现的失误,以及多线程程序的阅读技巧等。在讲解过程中,不仅以图配文,理论结合实例,而且提供了运用模式解决具体问题的练习题和答案,帮助读者加深对多线......一起来看看 《图解Java多线程设计模式》 这本书的介绍吧!