最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JavaScript函数式编程第二弹—函数组合(合成)

    正文概述 掘金(volibear)   2021-01-10   552

    函数组合

    函数式编程有两个最基本的运算:合成和柯里化。

    背景知识

    • 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))

    JavaScript函数式编程第二弹—函数组合(合成)

    比如:获取数组的最后一个元素再转换成大写字母

    // 使用lodash提供的API 先翻转数组 --> 再取第一个元素 --> 再转换成大写字母
    _.toUpper(_.first(_.reverse(array)))
    

    函数组合可以让我们把细粒度的函数重新组合生成一个新的函数,避免写出洋葱代码

    管道

    下面这个图表示了函数处理数据的过程,给fn输入参数a,得到结果b。可以理解成a数据通过管道fn得到b数据。 JavaScript函数式编程第二弹—函数组合(合成)

    下面这个图更是把管道fn拆分成了三个管道f1,f2,f3,数据a通过管道f3得到m,m通过管道f2得到n,n通过管道f1得到数据b。其实m和n是什么我们不用关心 JavaScript函数式编程第二弹—函数组合(合成)

    类似于下面的函数

    fn = compose(f1, f2, f3)
    b = fn(a)
    

    函数组合

    • 函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把所有中间步骤合并成一个函数
    • 函数组合默认是从右到左执行
    • 参与合成的函数也必须是纯的
    • f(x)和g(x)合成为f(g(x)),有一个隐藏的前提,就是f和g都只能接受一个参数。
    // 函数组合演示
    function compose(f, g) {
      return function (value) {
        return f(g(value))
      }
    }
    
    // 数组翻转函数
    function reverse (array) {
      return array.reverse()
    }
    
    // 获取第一个元素函数
    function first (array) {
      return array[0]
    }
    
    // 组合函数,获取函数最后一个元素
    const last = compose(first, reverse)
    
    console.log(last([1, 2, 3, 4])) // 4
    

    Lodash中的组合函数 —— flow()/flowRight()

    lodash 中组合函数 flow() 或者 flowRight(),都可以组合多个函数。

    • flow() 是从左到右运行
    • flowRight() 是从右到左运行

    下面实例是获取数组的最后一个元素并转化成大写字母

    const _ = require('lodash')
    
    const reverse = arr => arr.reverse()
    const first = arr => arr[0]
    const toUpper = str => str.toUpperCase()
    
    const f = _.flowRight(toUpper, first, reverse)
    
    console.log(f(['AAA', 'BBB', 'CCC'])) // CCC
    

    函数组合原理模拟

    上面的例子我们来分析一下:

    入参不固定,参数都是函数,出参是一个函数,这个函数要有一个初始的参数值

    function compose (...args) {
      // 返回的函数,有一个传入的初始参数即value
      return function (value) { // 因为前面提到:组合有一个隐藏的前提,函数都只能接受一个参数,所以不需要...arg来接收参数,只一个value就可以了
        // reduce的第一个参数是一个回调函数,第二个参数是acc的初始值,这里acc的初始值就是value
        return args.reverse().reduce(function (acc, fn) {
          return fn(acc)
        }, value)
      }
    }
    
    const fc = compose(toUpper, first, reverse)
    console.log(fc(['AAA', 'BBB', 'CCC'])) // CCC
    

    函数组合-结合律

    什么是函数组合结合律?

    下面三个情况结果一样,我们既可以把 g 和 h 组合,还可以把 f 和 g 组合。

    // 结合律(associativity)
    compose(f, compose(g, h))
    // 等同于
    compose(compose(f, g), h)
    // 等同于
    compose(f, g, h)
    

    函数组合-调试

    如果运行的结果和预期不一致,要怎么调试呢?怎么能知道中间运行的结果呢?

    下面输入ROYAL NEVER GIVE UP要对应输出royal-never-give-up(RNG加油!!!) 思路:先用空格将字符串分割为数组,将数组每个元素转为全小写,在将数组元素用'-'连接

    const _ = require('lodash')
    
    // _.split(string, separator),因为_.split方法第一个参数为需要分割的字符串,第二个参数为分割符
    // 且我们最后调用组合函数的时候要传入字符串,所以字符串要在第二个参数位置传入,因此需要二次封装一个split函数
    
    // 通过柯里化将多个参数转成一个参数
    const split = _.curry((sep, str) => _.split(str, sep))
    
    // 大写变小写,用到toLower(),因为这个函数本身只有一个参数,所以可以在函数组合中直接使用
    
    // _.join(array, [separator=',']) 
    // join方法也需要两个参数,第一个参数是数组,第二个参数是分隔符,数组也是最后的时候才传递,也需要进行柯里化转换
    const join = _.curry((sep, array) => _.join(array, sep))
    
    const f = _.flowRight(join('-'), _.toLower, split(' '))
    
    console.log(f('ROYAL NEVER GIVE UP')) // r-o-y-a-l-,-n-e-v-e-r,-g-i-v-e-,-u-p
    

    但是最后的结果却不是我们想要的,那应该怎么调试呢?

    const _ = require('lodash')
     
    const split = _.curry((sep, str) => _.split(str, sep))
    const join = _.curry((sep, array) => _.join(array, sep))
    
    // 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下
    const log = _.curry((tag, v) => {
      console.log(tag, v)
      return v
    })
    
    // 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么
    const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' '))
    // 从右到左
    // 第一个log:after split: [ 'ROYAL', 'NEVER', 'GIVE', 'UP' ] 正确
    // 第二个log: after toLower: royal,never,give,up  转化成小写字母的同时转成了字符串
    console.log(f('ROYAL NEVER GIVE UP')) // r-o-y-a-l-,-n-e-v-e-r,-g-i-v-e-,-u-p
    
    
    // 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写 
    // 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化
    const map = _.curry((fn, array) => _.map(array, fn))
    
    const f1 = _.flowRight(join('-'), map(_.toLower), split(' '))
    console.log(f1('ROYAL NEVER GIVE UP')) // royal-never-give-up
    

    FP模块

    函数组合的时候用到很多的函数需要柯里化处理,我们每次都处理那些函数有些麻烦,所以lodash中有一个FP模块

    • lodash 的 fp 模块提供了实用的对函数式编程友好的方法
    • 提供了不可变 auto-curried iteratee-first data-last (函数置先,数据置后)的方法
    // lodash 模块 
    const _ = require('lodash')
    // 数据置先,函数置后
    _.map(['a', 'b', 'c'], _.toUpper) 
    // => ['A', 'B', 'C'] 
    
    // 数据置先,分割符置后
    _.split('Hello World', ' ') 
    
    // lodash/fp 模块 
    const fp = require('lodash/fp') 
    
    // 函数置先,数据置后
    fp.map(fp.toUpper, ['a', 'b', 'c'])
    fp.map(fp.toUpper)(['a', 'b', 'c']) 
    // 分割符置先,数据置后
    fp.split(' ', 'Hello World') 
    fp.split(' ')('Hello World')
    

    体验FP模块对于组合函数的友好

    const fp = require('lodash/fp')
    
    const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
    
    console.log(f('ROYAL NEVER GIVE UP')) // royal-never-give-up
    

    Lodash-map方法的小问题

    const _ = require('lodash')
    const fp = require('lodash/fp')
    
    console.log(_.map(['55', '5', '11'], parseInt)) 
    // [ 55, NaN, 3 ]
    
    // _.map的回调函数接收三个参数,第一个参数是遍历的数组,第二个参数是key/index,第三个参数是对应函数
    _.map(['55', '5', '10'], function(...args){
      console.log(...args)
    })
    // 55 0 [ '55', '5', '11' ]
    // 5 1 [ '55', '5', '11' ]
    // 11 2 [ '55', '5', '11' ]
    
    // parseInt第二个参数表示进制,0默认就是10进制,1不存在2~36范围中,2表示2进制
    // parseInt('55', 0, array) 十进制
    // parseInt('5', 1, array)
    // parseInt('11', 2, array) 二进制
    
    // 要解决的话需要重新封装一个parseInt方法(默认基数为十进制)
    
    // 而使用fp模块的map方法不存在下面的问题
    console.log(fp.map(parseInt, ['55', '5', '11'])) 
    // [ 55, 5, 11 ]
    

    附上parseInt的使用,忘记的同学快速复习一下 JavaScript函数式编程第二弹—函数组合(合成)

    Point Free

    是一种编程风格,具体的实现是函数的组合。

    fn = R.pipe(f1, f2, f3); 这个公式说明,如果先定义f1、f2、f3,就可以算出fn。整个过程,根本不需要知道a(输入的数据)或b(最终得到的数据)。

    Point Free: 我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。

    • 不需要指明处理的数据
    • 只需要合成运算过程
    • 需要定义一些辅助的基本运算函数
    // 比如完成转换:Hello World => hello_world
    
    // 思路:
    // 先将字母换成小写,然后将空格换成下划线。如果空格多于一个,要替换成一个
    const fp = require('lodash/fp')
    
    // replace方法接收三个参数
    // 第一个是正则匹配pattern,第二个是匹配后替换的数据,第三个是要传的字符串
    // 所以这里需要传两个参数
    const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
    
    console.log(f('Hello World')) //hello_world
    

    Pointfree案例

    // world wild web -->W. W. W
    // 思路:
    // 把一个字符串中的单词首字母提取并转换成大写,使用. 作为分隔符
    const fp = require('lodash/fp')
    
    const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
    console.log(firstLetterToUpper('world wild web')) // W. W. W
    
    // 上面的代码进行了两次的遍历,性能较低
    // 优化
    const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
    console.log(firstLetterToUpper('world wild web')) // W. W. W
    

    Pointfree本质

    Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。

    比如,读取对象的role属性,不要直接写成obj.role,而是要把这个操作封装成函数。

    var prop = (p, obj) => obj[p];
    var propRole = R.curry(prop)('role');
    

    附录

    • 函数式编程指北
    • 函数式编程入门
    • Pointfree 编程风格指南

    起源地下载网 » JavaScript函数式编程第二弹—函数组合(合成)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元