介绍
函数式有不少处理输入参数的工具方法
identity
function identity(v) { return v}复制代码
unary
function unary(fn) { return function onlyOneArg(arg) { return fn(arg) }}复制代码
spreadArgs
function spreadArgs(fn) { return function spreadFn(argsArr){ return fn(...argsArr) }}复制代码
gatherArgs
function gatherArgs(fn) { return function gatheredFn(...argsArr) { return fn(argsArr) }}复制代码
reverseArgs
function reverseArgs(fn) { return function argsReversed(...args) { return fn(...args.reverse()) }}复制代码
...
...一切都很简单,确实,有的函数(如identity)简单到你可能都不知道它能用来做什么
函数式里的函数就像积木一样,每块积木看上去都是这么简单 至于怎么"堆"积木,可以用compose来组合他们,以后再关注这些方法的具体使用这次主要是来学习一下,partial和curry这两个对输入参数处理的方法,其实它们是js本身就有的功能,只不过函数式更多使用它们作为工具。
partial
举个例子, 你有一个ajax函数
function ajax(url, data, callback) { // ... }复制代码
你事先知道url, 但是data, callback可能要等一会(比如等用户输入完表单)才知道
(当然你可以等输入参数都ok,再调用) 这里可以创建一个新函数,内部调用ajax,并传入url, 等待data, callback参数function getPerson(data,cb) { ajax( "http://some.api/person", data, cb );}复制代码
function getOrder(data,cb) { ajax( "http://some.api/order", data, cb );}复制代码
很快,手动操作,就会变得很无趣,特别是如果已知参数变化,比如我们不仅事先知道url还知道data
function getCurrentUser(cb) { getPerson( { user: CURRENT_USER_ID }, cb );}复制代码
这时候我们需要寻找一个较为通用的工具方法 仔细观察发现,我们将部分实参预先应用到形参,而将剩余的实参推迟应用
partial(部分应用或偏函数应用) -- 其可以减少函数的输入参数的个数
// sq:function partial(fn, ...presetArgs) { return function partiallyApplied(...laterArgs) { return fn(...presetArgs, ...laterArgs) }}复制代码
使用
const getPerson = partial( ajax, "http://some.api/person" )const getOrder = partial( ajax, "http://some.api/order" )// 当然你可以写成 partial(ajax, 'http://xxx' { user: CURRENT_USRE_ID })// 但下面这种更合适一些const getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } )// 为方便理解,展开一下getCurrentUservar getCurrentUser = function outerPartiallyApplied(...outerLaterArgs){ var getPerson = function innerPartiallyApplied(...innerLaterArgs){ return ajax("http://some.api/person", ...innerLaterArgs); } return getPerson({ user: CURRENT_USER_ID }, ...outerLaterArgs)}复制代码
例2
function add(x, y) { return x + y}// [11, 12, 13][1, 2, 3].map(function adder(val) { return add(val + 10)})// 改用partial来将add函数适配map回调函数[1, 2, 3].map(partial(add, 10))复制代码
partialRight
如果上面的函数,我们预先知道的是data和callback, 而暂时不知道url呢?
版本一,使用前面的reverseArgs(反转参数)及partial
function partialRight(fn,...presetArgs) { return reverseArgs( partial( reverseArgs( fn ), ...presetArgs.reverse() ) )}// 使用function add(a, b, c, d) { return a + b * 2 + c * 3 + d * 4}const add2 = partialRight(add, 30, 40)// 10 + 20 * 2 + 30 * 3 + 40 * 4 = 300add2(10, 20)// 理解partialRight// reverseArgs(fn)返回一个函数,调用fn时将参数反转let fn2 = function argsReversed(...args) { return fn(...args.reverse())}// partial(reverseArgs(fn), ...presetArgs.reverse())的返回函数// 试想一下,调用p,会调用fn(...laterArgs.reverse(), ...presetArgs)// 所以需要对laterArgs也反转一次参数,fn(...lasterArgs, ...presetArgs)let p = function partiallyApplied(...laterArgs) { return fn2(...presetArgs.reverse(), ...laterArgs)}复制代码
建议还是敲一下代码,或者在纸上写一下
版本二 实际上,版本一可以当个练习,帮助理解,其实可以更直接
// sq:function partialRight(fn, ...presetArgs) { return function partiallyApplied(...laterArgs) { return fn(...laterArgs, ...presetArgs) }}复制代码
curry
curry即柯里化,将一个接收多个参数的函数分解一个连续的链式函数,其中每个函数接收一个参数而返回另一个函数接收下一个参数。(宽松型curry每个函数可以接收多个参数)
上例ajax, 如果使用curry
var personFetcher = curriedAjax( "http://some.api/person" )var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } )getCurrentUser( function foundUser(user){ /* .. */ } );复制代码
curry和partial很相似,但curry返回的函数,只能接收下一个参数
// sq:function curry(fn, arity = fn.length) { return (function nextCurried(prevArgs) { return function curried(nextArg) { var args = [...prevArgs, nextArg] if (args.length >= arity) { return fn(...args) } else { return nextCurried(args) } } })([])}复制代码
注意: 函数默认值形式,析构, 展开运算符形式...会导致fn.length不正确,所以此时应传入函数正确接收参数个数
之前的例2,使用curry
function add(x, y) { return x + y}var adder = curry( add )[1, 2, 3].map(adder(10))复制代码
例3
function sum(...nums) { var total = 0; for (let num of nums) { total += num; } return total;}// 15sum(1, 2, 3, 4, 5)var curriedSum = curry(sum, 5)// 15curriedSum(1)(2)(3)(4)(5)复制代码
实际上使用partial也是可以做到只接收一个参数,只需要一直对部分应用的函数手动连续调用partial,而curry则是自动。
function add(a, b, c) { return a + b + c}const partial1 = partial(add, 1)const partial2 = partial(partial1, 2)const partial3 = partial(partial2, 3)// 6partial3()复制代码
参数传入次数太多有时也挺麻烦,所以也允许curry传入多个参数,大部分库也是这样做的
// sq:function looseCurry(fn, arity = fn.length) { return (function nextCurried(prevArgs) { return function curried(...nextArgs) { var args = [...prevArgs, ...nextArgs]; if (args.length >= arity) { return fn(...args); } else { return nextCurried(args); } }; })([]);}复制代码
当然curry也有curryRight,暂时不过多介绍,以后用到再写
小结
库的真实实现可能略有区别,因为它毕竟要考虑更多方法的通用作出更高层次的抽象,但是上面的代码已经很好的可以帮助我们理解partial和curry,从而窥见函数式的冰山一角
这里值得一提的是我们在partialRight
版本一, 使用了.reverse()
, 这个是数组的一个变异方法 let arr = [1, 2, 3]arr.reverse()// [3, 2, 1]console.log(arr)复制代码
而变异方法会导致函数"不纯",从而又要引出一个纯函数的概念,这个以后想到时会提及一些,不过想理解这个"纯"最好还是看看文章
至于代码风格,你要是更喜欢ES6的箭头函数,完全可以依据喜好,当然命名函数是有诸多好处的,比如可读性,调试等