咱们常常说在Javascript语言中,函数是“一等公民”,它们本质上是十分简单和过程化的。能够利用函数,进行一些简单的数据处理,return
结果,或者有一些额外的功能,须要经过使用闭包来实现,最后常常会return
匿名函数。html
若是你对函数式编程有必定了解,函数柯里化(function currying)是不可或缺的,利用函数柯里化,能够在开发中很是优雅的处理复杂逻辑。git
柯里化(Currying),维基百科上的解释是,把接受多个参数的函数转换成接受一个单一参数的函数
先看一个简单例子github
// 柯里化 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
npm
function sum(a) { return function(b) { return function(c) { return a + b + c; } } }
固然,柯里化不是为了解决面试题,它是应函数式编程而生,编程
仍是看看上面的经典面试题。
若是想实现 sum(1)(2)(3)(4)(5)...(n)
就得嵌套n-1
个匿名函数,segmentfault
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
就会报错,咱们就能够设计成相似这样app
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函数做为回调函数就有很大优点。
有人说柯里化是应函数式编程而生,它在里面出现的几率就很是大了,在JS 函数式编程指南中,开篇就介绍了柯里化的重要性。
函数柯里化能够用来构建复杂的算法 和 功能, 可是滥用也会带来额外的开销。
从上面实现部分的代码中,能够看到,使用柯里化函数,离不开闭包, arguments, 递归。
闭包,函数中的变量都保存在内存中,内存消耗大,有可能致使内存泄漏。
递归,效率很是差,
arguments, 变量存取慢,访问性不好,