上一篇介绍了闭包和高阶函数, 这是函数式编程的基础核心. 这一篇来看看高阶函数的实战场景.
首先强调两点:
注意闭包的生成位置, 清楚作用域链, 知道闭包生成后缓存了哪些变量
高阶函数思想: 以变量作用域作为根基, 以闭包为工具来实现各种功能
柯里化(curry)
定义: 柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程.
先看个简单的例子, 这是一个名为 add 的函数: const add = (x, y) => x + y; 调用该函数 add(1, 1),add(1, 2),add(1, 3)... 很普通, 缺乏灵活性.
下面是柯里化实现版本:
const addCurried = x => y => x + y;
如果我们用一个单一的参数调用 addCurried,const add1 = addCurried(1)它返回一个函数 fn = y => 1 + y, 在其中 x 值通过闭包缓存下来. 接下来, 我们继续传参 add1(1); add1(2); add1(3), 有没有感觉比上面的 add 灵活.
上面的实现只是针对接收两个参数相加的柯里化函数, 接下来正是开始实现个基础的通用的接收两个参数的柯里化函数:
- const curry = (binaryFn) => {
- return function (firstArg) {
- return function (secondArg) {
- return binaryFn (firstArg, secondArg) ; // 为啥要嵌套那么多呢? 基于什么思路呢? 思考一下...
- };
- };
- };
现在可以用如下方式通过 curry 函数把 add 函数转换成一个柯里化版本:
- const autoCurriedAdd = curry(add)
- autoCurriedAdd(1)(1) // 2
这里我们已经体会到柯里化的好处了, 那么柯里化是怎样实现的呢? 看上面 curry 的实现很容易发现, 先传入一个接受二元函数, 然后返回一个一元函数, 当这个一元函数执行后, 再返回一个一元函数, 再次执行返回的一元函数时, 触发最开始那个二元函数的执行.
这里有一个点很重要 -- 执行时机, 接收够两个参数 (add 函数接收的参数数量) 立即执行, 也就是说接收够被柯里化函数的参数数量时触发执行.
好的, 我们已经实现了一个基础的柯里化函数. 不过, 这个 柯里化函数有很大的局限性 -- 只能用于接收两个参数的函数. 我们需要的是被柯里化函数的参数可以任意数量, 怎么办呢? 还好我们已经知道了被柯里化函数的执行时机 -- 接收够被柯里化函数的参数数量时触发执行. 下面我们来实现更复杂的柯里化:
- // 柯里化函数
- const curry = (fn) => {
- if (typeof fn !== 'function') {
- throw Error('No function provided')
- }
- return function curriedFn (...args) {
- if (fn.length> args.length) { // 未达到触发条件, 继续收集参数
- return function () {
- return curriedFn.apply(null, args.concat([].slice.call(arguments)))
- }
- }
- return fn.apply(null, args)
- }
- }
这样, 我们就能处理多个参数的函数了. 比如:
- const multiply = (x, y, z) => x*y*z;
- const curryMul = curry(multiply);
- const result = curryMul(1)(2)(3); // 1*2*3 = 6
偏应用(partial)
偏应用, 又称作部分应用, 它允许开发者部分地应用函数参数. 实际上, 偏应用是为一个多元函数预先提供部分参数, 从而在调用时可以省略这些参数.
比如我们要在每 10ms 做一组操作. 可以通过 setTimeout 函数以如下方式实现:
- setTimeout( () => console.log("Do X task"), 10);
- setTimeout( () => console.log("Do Y tash"), 10);
很显然, 我们可以用上面的 curry 函数包装成柯里化函数, 实现灵活调用:
- // 实现一个二元函数, 用于柯里化
- const setTimeoutWrapper = (time, fn) => {
- setTimeout(fn, time);
- }
- // 使用 curry 函数封装 setTimeout 来实现一个 10ms 延迟
- const delayTenMs = curry(setTimeoutWrapper)
- delayTenMs( () => console.log("Do X task") );
- delayTenMs( () => console.log("Do Y task") );
很棒, 也能实现灵活调用. 但问题是我们不得不创建 setTimeoutWrapper 一样的封装器, 这也是一种开销. 下面我们看看偏应用的实现:
- // 偏应用函数
- const partial = (fn, ...partialArgs) => {
- let args = partialArgs
- return (...fullArguments) => {
- let count = 0
- for (let i = 0; i <args.length && count < fullArguments; i++) {
- if (args[i] === undefined) {
- args[i] = fullArguments[count++]
- }
- }
- return fn.apply(null, args)
- }
- }
下面用偏应用解决上面的延时 10ms 问题:
- let delayTenMs = partial(setTimeout, undefined, 10); // 注意此处, 让我们少创建了一个 setTimeoutWrapper 封装器
- delayTenMs( () => console.log("Do X task") )
- delayTenMs( () => console.log("Do Y task") );
现在我们对柯里化有了更清晰的认识. 创建偏应用函数时, 第一个参数接收一个函数, 剩余参数是第一个传入函数所需参数. 剩余参数待传入的用 undefined 占位, 执行偏应用函数时填充 undefined.
组合(compose)
在了解什么是函数式组合之前, 让我们理解组合的概念.
符合 "|" 被称为管道, 它允许我们通过组合一些函数去创建一个能够解决问题的新函数. 大致来说,"|" 将最左侧的函数输出作为输入发送给最右侧的函数! 从技术上讲, 该处理过程称为 "管道".
compose 函数:
const compose = (a, b) => (c) => a(b(c))
compose 函数会首先执行 b 函数, 并将 b 的返回值作为参数传递给 a. 该函数调用的方向是从右至左的(先执行 b, 再执行 a).
可以看到, 组合函数 compose 就是传入一些函数. 对于传入的函数, 我们要求一个函数只做一件事.
下面看下如何应用 compose 函数:
- // 通过组合计算字符串单词个数
- let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
- let count = (array) => array.length; // 计算长度
- const countWords = compose(count, splitIntoSpaces);
- countWord("hello your reading about composition"); // 5
上面的 compose 只能实现两个函数的组合. 如何组合更多个函数呢? 这就需要借助 reduce 的威力了:
- // 组合多个函数 composeN
- const composeN = (...fns) =>
- (value) =>
- fns.reverse().reduce((acc, fn) => fn(acc), value);
管道 / 序列(pipe)
管道和组合的概念很类似, 都是串行处理数据. 唯一区别就是执行方向: 组合从右向左执行, 管道从左向右执行.
- // 组合多个函数 pipe
- const pipe= (...fns) =>
- (value) =>
- fns.reduce((acc, fn) => fn(acc), value);
下面看下如何应用 pipe 函数:
- // 通过管道计算字符串单词个数
- let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
- let count = (array) => array.length; // 计算长度
- const countWords = pipe(splitIntoSpaces, count); // 注意此处的传参顺序
- countWord("hello your reading about composition"); // 5
总结
通过这一节的学习, 我们知道了高阶函数的一些应用 -- 柯里化, 偏应用, 组合和管道, 每种应用都有特定的应用场景.
其中, 柯里化是最常用的一种场景, 它的作用是把一个多参数函数转换为一个嵌套的一元函数的过程. 随着闭包的产生, 我们可以灵活的调用.
组合和管道类似, 都是串行处理数据. 传入一个初始数据, 通过一系列特定顺序的纯函数处理成我们希望得到的数据.
参考链接:
简明 JavaScript 函数式编程 -- 入门篇 https://juejin.im/post/5d70e25de51d453c11684cc4
来源: https://www.cnblogs.com/chenwenhao/p/11708105.html