「前端进阶」完全弄懂函数组合

引言

函数组合在函数式编程中被称为组合(composition),咱们将了解组合的概念并学习大量的例子。而后建立本身的compose函数。javascript

组合的概念是很是直观的,并非函数式编程独有的,在咱们生活中或者前端开发中到处可见。前端

好比咱们如今流行的 SPA (单页面应用),都会有组件的概念,为何要有组件的概念呢,由于它的目的就是想让你把一些通用的功能或者元素组合抽象成可重用的组件,就算不通用,你在构建一个复杂页面的时候也能够拆分红一个个具备简单功能的组件,而后再组合成你知足各类需求的页面。java

其实函数组合也是相似,函数组合就是一种将已被分解的简单任务组合成复杂任务的过程。node

什么是组合

先看一个在 Linux 系统中经常使用的命令 ps -ef | grep nodegit

这个命令的用处是将系统中与 node 有关的进程显示出来,其中ps -ef是显示全部进程的全格式,grep node是过滤与node有关的内容,|是将左侧的函数的输出做为输入发送给右侧的函数。github

这个例子可能微不足道,但它传达了这样一个理念:编程

每个程序的输出能够是另外一个还没有可知的程序的输入设计模式

按照咱们对组合的理解,现假定有compose函数能够实现以下功能:数组

function compose(...fns){
    //忽略
}
// compose(f,g)(x) === f(g(x))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m,n)(x) === f(g(m(n(x))))
//···
复制代码

咱们能够看到compose函数,会接收若干个函数做为参数,每一个函数执行后的输出做为下一个函数的输出,直至最后一个函数的输出做为最终的结果。微信

应用 compose 函数

在建立并完善咱们本身的compose函数前,咱们先来学习一下如何应用compose函数。

假定有这样一个需求:对一个给定的数字四舍五入求值,数字为字符型。

常规实现:

let n = '3.56';
let data = parseFloat(n);
let result = Math.round(data); // =>4 最终结果
复制代码

在这段代码中,能够看到parseFloat函数的输出做为输入传递给Math.round函数以得到最终结果,这是compose函数可以解决的典型问题。

compose函数改写:

let n = '3.56';
let number = compose(Math.round,parseFloat);
let result = number(n); // =>4 最终结果
复制代码

这段代码的核心是经过composeparseFloatMath.round组合到一块儿,返回一个新函数number

这个组合的过程就是函数式组合!咱们将两个函数组合在一块儿以便能及时的构造出一个新函数!

再举一个例子,假设咱们有两个函数:

let splitIntoSpaces = str => str.split(' ');
let count = array => array.length;
复制代码

现但愿构建一个新函数以便计算一个字符串中单词的数量,能够很容易的实现:

let countWords = compose(count,splitIntoSpaces);
复制代码

调用一下:

let result = countWords('hello your reading about composition'); // => 5
复制代码

开发中组合的用处

假设咱们有这样一个需求:给你一个字符串,将这个字符串转化成大写,而后逆序。

咱们的常规思路以下:

let str = 'jspool'

//先转成大写,而后逆序
function fn(str) {
    let upperStr = str.toUpperCase()
    return upperStr.split('').reverse().join('')
}

fn(str) // => "LOOPSJ"
复制代码

这段代码实现起来没什么问题,但如今更改了需求,须要在将字符串大写以后,将每一个字符拆开并封装成一个数组:

"jspool" => ["J","S","P","O","O","L"]

为了实现这个目标,咱们须要更改咱们以前封装的函数,这其实就破坏了设计模式中的开闭原则。

开闭原则:软件中的对象(类,模块,函数等等)应该对于扩展是开放的,可是对于修改是封闭的。

那么在需求未变动,依然是字符串大写并逆序,应用组合的思想来怎么写呢?

原需求,咱们能够这样实现:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

let toUpperAndReverse = compose(stringReverse, stringToUpper)
let result = toUpperAndReverse(str) // "LOOPSJ"
复制代码

那么当咱们需求变化为字符串大写并拆分为数组时,咱们根本不须要修改以前封装过的函数:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function stringToArray(str) {
    return str.split('')
}

let toUpperAndArray = compose(stringToArray, stringToUpper)
let result = toUpperAndArray(str) // => ["J","S","P","O","O","L"]

复制代码

能够看到当变动需求的时候,咱们没有打破之前封装的代码,只是新增了函数功能,而后把函数进行从新组合。

可能有人会有疑问,应用组合的方式书写代码,当需求变动时,依然也修改了代码,不是也算破坏了开闭原则么?其实咱们修改的是调用的逻辑代码,并无修改封装、抽象出来的代码,而这种书写方式也正是开闭原则所提倡的。

咱们假设,如今又修改了需求,如今的需求是,将字符串转换为大写以后,截取前3个字符,而后转换为数组,那么咱们能够这样实现:

let str = 'jspool'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function getThreeCharacters(str){
    return str.substring(0,3)
}

function stringToArray(str) {
    return str.split('')
}

let toUpperAndGetThreeAndArray = compose(stringToArray, getThreeCharacters,stringToUpper)
let result = toUpperAndGetThreeAndArray(str) // => ["J","S","P"]
复制代码

从这个例子,咱们能够知道,组合的方式是真的就是抽象单一功能的函数,而后再组成复杂功能,不只代码逻辑更加清晰,也给维护带来巨大的方便。

实现组合

先回看compose函数到底作了什么事:

// compose(f,g)(x) === f(g(x))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m)(x) === f(g(m(x)))
// compose(f,g,m,n)(x) === f(g(m(n(x))))
//···
复制代码

归纳来讲,就是接收若干个函数做为参数,返回一个新函数。新函数执行时,按照由右向左的顺序依次执行传入compose中的函数,每一个函数的执行结果做为为下一个函数的输入,直至最后一个函数的输出做为最终的输出结果。

若是compose函数接收的函数数量是固定的,那么实现起来很简单也很好理解。

只接收两个参数:

function compose(f,g){
    return function(x){
        return f(g(x));
    }
}
复制代码

只接收三个参数:

function compose(f,g,m){
    return function(x){
        return f(g(m(x)));
    }
}
复制代码

上面的代码,没什么问题,可是咱们要考虑的是compose接收的参数个数是不肯定的,咱们考虑用rest参数来接收:

function compose(...fns){
    return function(x){
        //···
    }
}
复制代码

如今compose接收的参数fns是一个数组,那么如今思考的问题变成了,如何将数组中的函数从右至左依次执行。

咱们选择数组的reduceRight函数来实现:

function compose(...fns){
    return function(x){
        return fns.reduceRight(function(arg,fn){
            return fn(arg);
        },x)
    }
}
复制代码

这样咱们就实现了compose函数~

实现管道

compose的数据流是从右至左的,由于最右侧的函数首先执行,最左侧的函数最后执行!

但有些人喜欢从左至右的执行方式,即最左侧的函数首先执行,最右侧的函数最后执行!

从左至右处理数据流的过程称之为管道(pipeline)!

管道(pipeline)的实现同compose的实现方式很相似,由于两者的区别仅仅是数据流的方向不一样而已。

对比compose函数的实现,仅需将reduceRight替换为reduce便可:

function pipe(...fns){
    return function(x){
        return fns.reduce(function(arg,fn){
            return fn(arg);
        },x)
    }
}
复制代码

组合相比,有些人更喜欢管道。这只是我的偏好,与底层实现无关。重点是pipecompose作一样的是事情,只是数据流放行不一样而已!咱们能够在代码中使用pipecompose,但不要同时使用,由于这会在团队成员中引发混淆。若是要使用,请坚持只用一种组合的风格。

系列文章推荐

参考

写在最后

  • 文中若有错误,欢迎在评论区指正,若是这篇文章帮到了你,欢迎点赞关注
  • 本文同步首发与github,可在github中找到更多精品文章,欢迎Watch & Star ★
  • 后续文章参见:计划

欢迎关注微信公众号【前端小黑屋】,每周1-3篇精品优质文章推送,助你走上进阶之旅

相关文章
相关标签/搜索