文章内容输出来源:拉勾教育 大前端高薪训练营
前言
在学习函数组合之前,我们需要先了解管道的概念。管道在不同的领域,有不同的定义。那么在开发当中,管道是什么呢?
一、管道
当在程序中使用函数处理数据的时候,我们可以把所使用的函数看成是一个管道,通过函数输入对应的参数 ,可以返回相应的结果。
如图所示:
一个程序的运行当中,管道的数量是不固定的,有的时候甚至可以把一个大的管道分割成多个小的管道,然后通过组合的方式,得到程序运行后的结果。
如图所示:
简单来说,管道经常用来将某个命令或程序的输出提供给另一个命令或程序。
代码如下(示例):
fn = compose(f1, f2, f3)
b = fn(a)
二、函数组合(compose)
说完了日常的管道概念,下面我们来说一下函数组合。
1. 什么是函数组合
- 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。也就是说,将需要嵌套执行的函数进行平铺。嵌套执行指的是,一个函数的返回值将作为另一个函数的参数。
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。
- 函数组合默认是从右到左执行。
2. 模拟函数组合的实现过程
-
一个简单的例子:
代码如下(示例):
// 函数组合演示 function compose (f, g) { // f, g 都是函数,并且都作为参数进行传入 return function (value) { // value 为输入的值 return f(g(value)) // 先执行g函数,再执行f函数,符合函数组合默认从右到左执行 } } // 箭头函数, 二选一, 此种方式更简便 // let compose = (f, g) => value => f(g(value)) let reverse = arr => arr.reverse() let first = arr => return array[0] // 先把数组进行反转,再进行取值 const last = compose(first, reverse) /** -- 程序运行过程 * last = (first, reverse) => value => first(reverse(value)) */ console.log(last([1, 2, 3, 4])); // 4
3. lodash 中的组合函数
lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数。
- flow() 是从左到右运行。
- flowRight() 是从右到左运行,使用的更多一些。
-
下面我们就来模拟一下flowRight()的实现过程:
代码如下(示例):
const _ = require('lodash'); const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase() // 多函数组合 function compose(...args) { // 需要接收的参数不确定,使用剩余参数写法 return function (value) { // 需要接收参数 // 因为需要从右往左执行,所以需要对数组进行反转 return args.reverse().reduce(function (acc, fn) { return fn(acc) }, value) } } // 箭头函数写法 const compose = (...args) => (value) => args.reverse().reduce((acc, fn) => fn(acc), value) /** -- 内部函数和reduce()执行过程 * args.reverse() = [reverse, first, toUpper] * 第一次迭代:acc = reverse, fn = first * 第二次迭代:acc = first(reverse), fn = toUpper * 第三次迭代:acc = toUpper(first(reverse)), 没有下一项 * 因此 迭代结束,返回 toUpper(first(reverse)) * * ==> 最终结果 * const f = function compose (toUpper ,first , reverse) { * return function (value) { * return toUpper(first(reverse)) * } * } * ==> * const = f = compose = (toUpper ,first , reverse) => value => toUpper(first(reverse)) */ const f = compose(toUpper ,first , reverse) console.log(f(['one', 'two', 'three'])) // THREE
reduce()具体用法,详情参见 数组(Array)的常用方法
-
函数的组合要满足结合律 (associativity):
我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的。代码如下(示例):
// 结合律(associativity) const _ = require('lodash') // const f = _.flowRight(_.toUpper, _.first, _.reverse) // const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse) const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse)) console.log(f(['one', 'two', 'three'])) // => THREE , 以上三种方式的结果相同
4. 调试
-
如何调试组合函数:
通过辅助函数记录日志,传入的参数为上一个函数的执行结果,然后观察每次返回的结果。代码如下(示例):
// NEVER SAY DIE --> never-say-die const _ = require('lodash') // tag 用作标记,v 表示实际传入的上一个函数的执行结果 const trace = _.curry((tag, v) => { console.log(tag, v) return v }) // 利用lodash中的curry()方法,将多个函数转换成单一函数,柯里化 const split = _.curry((sep, str) => _.split(str, sep)) const join = _.curry((sep, array) => _.join(array, sep)) // 对数组中的每一个元素进行处理,fn 决定如何去处理数组的每一项 const map = _.curry((fn, array) => _.map(array, fn)) // 会将split的结果返回给前一个函数 const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' ')) console.log(f("NEVER SAY DIE")) // never-say-die
5. lodash中的fp模块
在lodash中文网,关于它里面的fp模块介绍,有这样一句话:
Load the FP build for immutable auto-curried iteratee-first data-last methods
简单来说,就是lodash中的fp模块提供了不可变的三个特性,即:
-
auto-curried(自动柯里化)
-
iteratee-first(函数优先)
-
data-last(数据滞后)
代码如下(示例):
// lodash 中的 fp 模块 const fp = require('lodash/fp'); const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' ')) console.log(f("NEVER SAY DIE"));
-
lodash中方法 和 fp模块中方法 的不同
1、lodash中方法: 未柯里化,数据优先,函数滞后
2、fp模块中方法:已经被柯里化,函数优先,数据滞后下面我们通过代码的形式,lodash 和 lodash/fp 模块中 map 方法的区别:
代码如下(示例):
// const _ = require('lodash'); // console.log(_.map(['23', '8', '10'], parseInt)) // // parseInt('23', 0, array) // // parseInt('8', 1, array) // // parseInt('10', 2, array) const fp = require('lodash/fp') // 区别:接收参数不同 console.log(fp.map(parseInt, ['23', '8', '10']))。
6. Point Free
Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
-
不需要指明处理的数据
-
只需要合成运算过程
-
需要定义一些辅助的基本运算函数
案例演示:
// 非 Point Free 模式 // Hello World => hello_world function f (word) { return word.toLowerCase().replace(/\s+/g, '_'); } // Point Free const fp = require('lodash/fp') const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower) console.log(f('Hello World'))
-
使用 Point Free 的模式,把单词中的首字母提取并转换成大写
-
代码如下(示例):
const fp = require('lodash/fp') const firstLetterToUpper = fp.flowRight(join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' ')) console.log(firstLetterToUpper('world wild web')) // => W. W. W