小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
前言
在我们进入学习函数组合之前,先让我们回忆一下柯里化和纯函数
纯函数和柯里化很容易写出洋葱代码
h(g(f(x)))
啥是洋葱代码
像洋葱的切面一样一层一层的嵌套在一起的代码
'(当然还有面条式代码,这种代码才是最垃圾的,千万避免)'
让我们来一个实际一点的案例
_.toUpper(_.first(_.reverse(array)))
像这样的代码一层一层传递,一层包裹一层就是洋葱代码,使用函数组合就可以避免这种情况
管道
在我们深入了解函数组合的时候,我们先看看以下的内容:
什么是管道
下面这张图表示程序中使用函数处理数据,a参数输入给函数fn,在fn中经过各种逻辑处理从而得出结果b,这就是数据a通过管道fn之后变成了b。
管道拆分
上图中的管道看起来很方便,因为我们只需要维护这个管道就好
Q: 管道的长度、广度过大的时候,如果这个管道出现了一些纰漏,那我们排查起来的工作量就很大,很困难,该如何处理呢?
A: 拆分管道!
下面这张图我们将管道fn拆分成了f3、f2、f1,再通过参数a的输入之后,经过f3管道的处理获得m,
m在通过f2,如此进行,最终获得结果b
经过这样的拆分,我们可以保证每个函数的颗粒度,在之后出现问题的时候,我们可以更快更精准的进行问题打击,一击致命!bug来的快,去的更快!(ps:中间产生的中间结果我们是不需要进行考虑的)
我们使用伪代码描述一下这张图
fn = compose(f1,f2,f3)
b = fn(a)
函数组合(compose)
什么是函数组合
函数可以让我们把细粒度的函数重新组合生成一个新的函数
- 函数就像是数据管道,而函数组合就是把这些进行连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
lodash中的组合函数
flow
创建一个函数。 返回的结果是调用提供函数的结果,this 会绑定到创建函数。 每一个连续调用,传入的参数都是前一个函数返回的结果。
参数
[funcs](...(Function|Function[])) : 要调用的函数。 返回
(Function) ( 返回新的函数。)
例子
// 引入lodash
const _ = require('lodash')
function square(n) {
return n * n;
}
function add(augend,augend) {
return augend + augend;
}
const addSquare = _.flow([add, square]);
// 当然也可以这样使用
// const addSquare = _.flow(add, square);
addSquare(1, 2); // => 9
flowRight
这个方法类似_.flow,除了它调用函数的顺序是从右往左的。
参数
[funcs](...(Function|Function[])) : 要调用的函数。 返回
(Function) ( 返回新的函数。)
例子
// 引入lodash
const _ = require('lodash')
function square(n) {
return n * n;
}
function add(augend,augend1) {
return augend + augend1;
}
const addSquare = _.flowRight([square,add]) ;
// 当然也可以这样使用
// const addSquare = _.flowRight(square,add);
addSquare(1, 2); // => 9
手写函数组合demo
老话说得好,磨刀不误砍柴工,在我们下手之前,我们先依据lodash库中的两个函数做一下需求的整理吧!
- 函数可以接收参数,如果是数组的话,有且只能有一个,如果不为数组的话参数的数量不限
- 函数需要返回一个函数,且我们返回的函数要对数据进行处理 ok带着这些需求我们先整理一份简单的demo出来吧
1.获取参数
// 老规矩es6的剩余参数
function flow(...funs) {
console.log(funs)
}
ok,我们第一步已经完成了,不过有几个问题,我们如果传入flow(方法一,方法2) 打印出来的结果会是[方法一,方法2],但是我们传入flow([方法一,方法2])打印出来的结果会是[[方法一,方法2]],这样对我们的之后进行不是很方便,我们稍微做一下数组的扁平化吧!
2.数组扁平化
遍历数组每一项,若值为数组则递归遍历,否则concat。
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
但是我们要进行一下改造,刚刚我们的扁平化方法其实是进行递归扁平化的,但是我们不需要这样,这样就和我们传递的参数格式不一样了,改造改造,这样就好
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(item);
}, []);
}
3.第三步判断参数
既然我们的参数已经做了扁平化操作,那我们就简单了,遍历一次扁平化后的数组,进行一次判断,是否每个参数都是方法不是的话,就给他抛出错误
// 老规矩es6的剩余参数
function flow(...funs) {
// 这里我们要进行一下判断中是否都是方法体
let funList = flatten(funs)
const length = funList.length
let index = length
while (index--) {
if (typeof funList[index] !== 'function') {
throw new TypeError('Expected a function')
}
}
return function(...args) {
...
}
}
这里为什么用while不用for呢? 本来我是直接判断
```js
for(const i of funList){
if (typeof funList[index] !== 'function') {
throw new TypeError('Expected a function')
}
}
```
可是我在lodash实验的时候,发现其中的flow方法,可以不传入参数,输出为undefined的。做额外的判断太麻烦,就用while了
4. 第四步完结
在我们判断完毕参数之后,我们就要调用参数一个一个进行传参了,每次我们都会调用fun的中的方法体,然后获取其中传入的参数,当我们调用了等于我们传入的方法数量的时候,停止调用就好
// 老规矩es6的剩余参数
function flow(...funs) {
// 这里我们要进行一下判断中是否都是方法体
let funList = flatten(funs)
const length = funList.length
let index = length
while (index--) {
if (typeof funList[index] !== 'function') {
throw new TypeError('Expected a function')
}
}
return function(...args) {
let index = 0
let result = length ? funList[index].apply(this, args) : args[0]
while (++index < length) {
result = funList[index].call(this, result)
}
return result
}
}
测试
const addSquare1 = flow(add,square) // 预计9
console.log('直接传入函数:',addSquare1(1,2))
const addSquare2 = flow([add,square]) // 预计9
console.log('传入数组:',addSquare2(1,2)) // 预计结果:9
小小的优化
可是flowRight的怎么办?
再写一遍吗?不行太麻烦了,我们进行一下封装吧!
function create(isRight){
return function (...funs) {
let funList = flatten(funs)
if(isRight){
funList.reverse();
}
// 这里我们要进行一下判断中是否都是方法体
const length = funList.length
let index = length
while (index--) {
if (typeof funList[index] !== 'function') {
throw new TypeError('Expected a function')
}
}
return function(...args) {
let index = 0
let result = length ? funList[index].apply(this, args) : args[0]
while (++index < length) {
result = funList[index].call(this, result)
}
return result
}
}
}
这样我们使用 flow的时候这样运行就可以了const flow = create(),flowRight的时候这样运行就可以了 const flowRight = create(true)
测试一下
const flow = create()
const flowRight = create(true)
const addSquare1 = flow(add,square) // 预计9
console.log('flow直接传入函数:',addSquare1(1,2))
const addSquare2 = flow([add,square]) // 预计9
console.log('flow传入数组:',addSquare2(1,2)) // 预计结果:9
const addSquare3 = flowRight(square,add) // 预计9
console.log('flowRight直接传入函数:',addSquare3(1,2))
const addSquare4 = flowRight([square,add]) // 预计9
console.log('flowRight传入数组:',addSquare4(1,2)) // 预计结果:9
const addSquare5 = flow() // undefined
console.log('flow不传入函数:',addSquare5())
const addSquare6 = flowRight() // undefined
console.log('flowRight不传入函数:',addSquare6())
完整代码
function square(n) {
return n * n;
}
function add(augend,augend1) {
return augend + augend1;
}
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(item);
}, []);
}
function create(isRight){
return function (...funs) {
let funList = flatten(funs)
if(isRight){
funList.reverse();
}
// 这里我们要进行一下判断中是否都是方法体
const length = funList.length
let index = length
while (index--) {
if (typeof funList[index] !== 'function') {
throw new TypeError('Expected a function')
}
}
return function(...args) {
let index = 0
let result = length ? funList[index].apply(this, args) : args[0]
while (++index < length) {
result = funList[index].call(this, result)
}
return result
}
}
}
const flow = create()
const flowRight = create(true)
const addSquare1 = flow(add,square) // 预计9
console.log('flow直接传入函数:',addSquare1(1,2))
const addSquare2 = flow([add,square]) // 预计9
console.log('flow传入数组:',addSquare2(1,2)) // 预计结果:9
const addSquare3 = flowRight(square,add) // 预计9
console.log('flowRight直接传入函数:',addSquare3(1,2))
const addSquare4 = flowRight([square,add]) // 预计9
console.log('flowRight传入数组:',addSquare4(1,2)) // 预计结果:9
const addSquare5 = flow() // undefined
console.log('flow不传入函数:',addSquare5())
const addSquare6 = flowRight() // undefined
console.log('flowRight不传入函数:',addSquare6())
小结
到这里我们简单版本的组合函数已经解决了,但是还是有点问题的,思路没问题,希望看到的这里的同学给个赞吧!
可以的话也拿去试试看,一起发现问题一起进步!