小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
前言
在我们进入学习函数组合之前,先让我们回忆一下柯里化和纯函数
在我们回忆完毕这两个概念之后,让我们来看个问题:
纯函数和柯里化很容易写出洋葱代码
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(Array.isArray(item) ? [...item] : item);
}, []);
}
复制代码
3.第三步 完结版本
既然我们的参数已经做了扁平化操作,那我们就简单了,遍历一次扁平化后的数组,进行一次判断,是否每个参数都是方法不是的话,就给他抛出错误
// 老规矩es6的剩余参数
function flow(...funs) {
// 这里我们要进行一下判断中是否都是方法体
let funList = flatten(funs)
const length = funList.length
for(const fun of funList){
if (typeof fun !== '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
for(const fun of funList){
if (typeof fun !== '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
复制代码
完整代码
function square(n) {
return n * n;
}
function add(augend,augend1) {
return augend + augend1;
}
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? [...item] : item);
}, []);
}
function create(isRight){
return function (...funs) {
let funList = flatten(funs)
if(isRight){
funList.reverse();
}
// 这里我们要进行一下判断中是否都是方法体
const length = funList.length
for(const fun of funList){
if (typeof fun !== '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
复制代码
小结
到这里我们简单版本的组合函数已经解决了,但是还是有点问题的,思路没问题,希望看到的这里的同学给个赞吧!
可以的话也拿去试试看,一起发现问题一起进步!