一、柯里化 - currying
核心代码实现:
export function curry(fn) { let len = fn.length; // 收集目标函数的参数个数(总) return function curriedFn(...params) { // 说明当前收集到的参数还没有达到目标函数需要接收到的,需要返回一个新的函数继续去收集 if (params.length < len) { return function () { // 一个关键是下面这个 arguments 是属于当前这个匿名函数的 // 另外一个关键是 params.concat(...), 这里其实应用了尾调用优化 return curriedFn.apply( null, params.concat([].slice.call(arguments)) ) } } return fn.apply(null, params); } }
let curriedSum = curry(sum); // curriedSum 是一个函数 let tmpA = curriedSum(1); // tmpA 是一个函数 let tmpB = tmpA(2); // tmpB 是一个函数 let tmpC = tmpB(3); // tmpC 则是最终的运算结果,就是原函数 sum(1, 2, 3) 的运算结果,即不是一个函数
- 执行第一句, curry 函数执行并形成闭包(暂时命名成 curry-[fn] ),len 记录为 3,返回 curriedFn ,即 curriedSum 就是 curriedFn
- 执行第二句,其实执行的是 curriedFn(1) ,此时 params = [1] ,进入逻辑判断, curriedFn 形成一层闭包(暂时命名成 curriedFn-[1] ),返回匿名函数,即 tmpA 就是这个匿名函数
- 执行第三句,运行这个匿名函数,用伪代码表示如下,这句有两个关键点:
let tmpA = function () { return curriedFn.call(null, params.concat([].slice.call(arguments))) } tmpA(2);
- 首先是 arguments,这个变量属于这个匿名函数,而在此时变量的值就是 [2] (传入的变量就是 2,如果是 tmpA(2, 3) 这种,那么 arguments = [2, 3] )
- 其次 params.concat([].slice.call(arguments)) ,这个结合缓存的结果就是 [1].concat([2]) ,即 [1, 2] ,然后将这个值作为参数进行递归 curriedFn([1, 2]) ,所以说这里其实应用了尾递归优化
所以经过运算,最终 tmpB 也是那个匿名函数,而第二句中不是形成了一个 curriedFn-[1] 的闭包嘛,此时也被释放,而随着 curriedFn([1, 2]) 的执行,形成一个新的闭包(暂时命名成 curriedFn-[1, 2] ),在这个新的闭包中, params = [1, 2]
- 执行第四句,走的是 curriedFn 中 return fn.apply(null, params); 这句,得出最终的运算结果,这里就不详叙述
其他
组合指的是将多个函数组合成一个函数,这样一个函数的输出就可以作为另一个函数的输入,从而实现多个函数的链式调用
组合compose可以提高代码的可读性和可维护性,减少重复代码的出现,更加便捷的实现函数的复用
用表达式表示: compose(f, g, t) => x => f(g(t(x))) ,进一步结合柯里化则是 compose(f)(g)(t) => x => f(g(t(x)))
概念上, compose 函数像是 curry 的逆运算,把多个函数链接起来依次调用,不用过于关注其中的执行过程,直接得到最终结果,这样最直观的好处是节省了一堆临时变量核心代码实现:
// 普通版 export const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); // 异步版 export const compose = (...fns) => (input) => fns.reduceRight((chain, fn) => chain.then(fn), Promise.resolve(input)); // 普通版支持链接后多参数入参,而异步版则只支持单参数(更符合函数式编程思想)
三、管道 - pipe
管道其实是组合的另外一个版本,组合是从右向左依次调用处理函数,使用的是 reduceRight ,而管道则是从左向右依次调用处理函数,使用的是 reduce 函数
四、实践经验
一、柯里化中把要操作的数据放到最后
// 推荐 const target = (x, str) => str.split(x); // 不推荐 const target = (str, x) => str.split(x);
二、函数组合中函数要求单输入
即:传给 compose 函数的参数,最好是经过 curry 化的函数
三、函数组合的 Debug
// debugger 函数,其中 x 是 reverse 这个函数的计算值 const trace = curry((tip, x) => { console.log(tip, x); return x; }); const fn = compose(toUpperCase, head, trace('after reverse'), reverse);