js高级-函数式编程(纯函数,柯里化)& 组合函数

212 阅读12分钟
javaScript是非常灵活的,他的灵活处体现之一就是它是支持多种支持多种编程范式的。
js是支持函数式编程的。

怎么理解函数式编程?

(1)简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。

(2)它属于["结构化编程"] 的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

(3)函数式编程是一种编程方式,它不只存在于javaScript,也存在于其他语言。

(4)在js中,函数式编程的实现主要在于在编码过程中尽量的使用纯函数编写代码逻辑。

函数编程中的两个重要概念

1.纯函数 
   纯函数是指满足如下两个条件的函数:
       (1)确定的输入,一定会产生确定的输出;
       (2)函数在执行过程中,不能产生副作用;
2.柯里化:传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数

函数式编程的优点

1. 代码简洁,开发快速

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

2. 接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码。

3. 更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

纯函数

什么是纯函数?

纯函数的维基百科定义

在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
(1)此函数在相同的输入值时,需产生相同的输出。
(2)函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
(3)该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
简单来说,一个具备如下两个条件的函数就是一个纯函数
(1)确定的输入,一定会产生确定的输出
(2)函数在执行过程中,不能产生副作用

副作用的理解

 (1)副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一 些其他的副作用;
 (2)在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生 了附加的影响,
比如修改了全局变量,修改参数或者改变外部的存储;
 (3)纯函数在执行的过程中就是不能产生这样的副作用: 
    副作用往往是产生bug的 “温床”。

开发中为什么要写纯函数

    (1)纯函数是函数式编程中有一个非常重要的概念,掌握它毫无疑问可以使我们的代码更符合函数编程的规范,也就使我们的代码
具有函数式编程的优点。比如代码更简洁,更易于管理,也更容易理解。
    另外前款框架中的react中多次提到纯函数概念,react中组件就被要求像是一个纯函数,redux中有一个reducer的概念,也是
要求必须是一个纯函数;所以掌握纯函数对于我们更好的使用框架,编写一些高质量的代码有绝对的好处。

纯函数有什么优点

1.纯函数可以安心的编写和安心的使用;
2.写纯函数的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
3.在使用纯函数的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出

纯函数理解案例

案例一:slice 是一个纯函数

    var names = ["abc","cba","nba","dna"]
    var newNames = names.slice(0,3)
    console.log(newNames)  // [ 'abc', 'cba', 'nba' ]
    console.log(names) //[ 'abc', 'cba', 'nba', 'dna' ]

slice 只要给它传入一个start/end,那么对于同一个数组来说,他会给我们返回确定的值。 slice函数本身不会修改原来的数组,所以他没有副作用 所以我们说slice 是一个纯函数

案例二:splice 不是一个纯函数

    var names = ["abc","cba","nba","dna"]
    var newNames = names.splice(2)
    console.log(newNames) //[ 'nba', 'dna' ]
    console.log(names)// [ 'abc', 'cba' ]

splice 在执行时,有修改调用的数组本身,修改的这个操作就是这个函数产生的副作用,所以splice 不是一个纯函数

案例三:foo函数 是一个纯函数

    function foo(num1, num2) {
      return num1 * 2 + num2 * num2
    }

1.相同的输入一定产生相同的输出 2.在执行的过程中不会产生任何的副作用 满足条件,所以foo函数是一个穿函数

案例四:bar函数不是一个纯函数

    var name = "abc" 
    function bar() {
      console.log("bar其他的代码执行")
      name = "cba"
    }

bar()
console.log(name)

修改了外界的值

案例五:baz不是一个纯函数

    function baz(info) {
      info.age = 100
    }

    var obj = {name: "why", age: 18}
    baz(obj)
    console.log(obj)

这里baz函数修改了 obj 这个外界的对象的值,并且没有将这个对象进行返回,它只是单纯的修改了外界的变量,产生了一定的副作用。

案例六: test是一个纯函数

    function test(info) {
      return {
        ...info,
        age: 100
      }
    }

    test(obj)
    test(obj)

虽然这里看着貌似是修改了info这个外界对象的值,但是我们的修改过程造成的结果是满足同样的输入有同样的结果的。

柯里化

什么是柯里化

维基百科的解释

(1)在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;

(2)是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参 数,而且返回结果的新函数的技术;

(3)柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数” 简化理解:

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化;

柯里化案例

一般函数的实现

    function add(x,y,z) {
        return x + y + z
    }
    var result = add(10,20,30)
    console.log(result)

柯里化后的函数实现

    function add(x) {
        return function(y){
            return function(z){
                return x + y + z
            }
        }
    }
    var result = add(10)(20)(30)
    console.log(result)

可以看到经过柯里化后,首先我们函数的调用方式发生了变化,这种方式的变化,可以让我们更清晰的理解这个函数都做了什么事情。其次,我们发现柯里化后,我们对函数的整体逻辑做了清晰的划分,也是对每个参数的处理进行了单独的封装,这样,使得柯里化过程中的每个函数的职责单一,对函数的封装,也为代码的复用提供了可能。下面对柯里化的优点做一个总结

柯里化的优点

让函数的职责单一

在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个 函数来处理;那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结 果;

通过上面的柯里化的案例,我们可以很容易做到这点,通过让把对函数参数的处理的过程封装成函数,使得处理参数的每个函数的指责单一,这就是柯里化的优点之一。

复用参数处理逻辑

我来看下面的代码

    // 不复用参数逻辑
    function sum(m,n) {
        return m + n 
    }
    //调用
    console.log(sum(5,10))
    console.log(sum(5,100))
    console.log(sum(5,1000))

当我们没有使用柯里化时,每次调用sum函数都要传递5这个参数

    //使用柯里化,复用逻辑
    function makeAdder(count) {
        count = count * count
        return function(num){
            return count + num
        }
    }

    var adder5 = makeAdder(5) // 这里我们拿到makeAdder5 的返回函数,相当于对count这个参数的逻辑进行了封装,任何情况下,需要这个逻辑,我们不需要在传递5这个参数的逻辑,直接调用这个函数,就能复用对参数5的逻辑的复用

    adder(10)
    adder(100)

上面的这个例子相对简单,不容易看出柯里化函数的优点,我么看如下的代码。 假如我们想写一个简易的输出函数,期望能够辅助开发,起到打印代码流程的作用。 不使用柯里化,我们可以这样做

    function log(date,type,message){
        console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]:[${message}]`)
    }
    
    log(new Date(),"DEBUG","查找轮播图的bug")
    log(new Date(),"DEBUG","查找彩蛋的bug")
    log(new Date(),"FETURE","新增了添加用户的功能")

可以看到不使用柯里化,这个函数功能也能实现,但是new Date()这个逻辑是固定的,且存在重复,而且type这个参数的逻辑我们也可以通过柯里化实现定制,来复用对应的逻辑 柯里化实现

    // 这里我们使用箭头函数来实现函数的柯里化
    var log = date => type => message => {
        console.log([${date.getHours()}:${date.getMinutes()}][${type}]:[${message}])
    }
    //首先可以复用创建时间的操作,我们可以取到赋值时间后的函数,用这个函数进行后续的函数逻辑处理,这样就不用每次都传递时间参数。
    var nowlog = log(new Date())
    newlog("DEBUG","查找轮播图的bug")
    newlog("FETURE","新增了添加用户的功能")


    //然后 我们还可以实现对type参数逻辑的复用,对输出的函数进行进一步的定制,这样我们在函数调用时,只需要传递最后一个参数即可。
    var nowAndDebugLog = log(new Date())("DEBUG")
    //当需要打印bug日志时,我们可以直接通过调用定制的函数nowAndDebugLog实现。
    nowAndDebugLog("查找到轮播图的bug")
    //同样可以定制打印添加新功能的函数
    var nowAndFetureLog = log(new Date())("FETURE")
    nowAndFetureLog("添加新功能~")

柯里化自动化函数编写

虽然柯里化后的函数使用起来很灵活,优点也很多,但是每次都对函数进行手动的柯里化,过程会很繁琐,会增加写代码的时间,我们可以写一个自动柯里化的函数来实现对函数的柯里化

    function add1(x, y, z) {
      x = x + 2;
      y = y * 10;
      z = z + 5;
      return x + y + z;
    }
    
    function add1(x){
      x = x + 2
      return function(y){
        y = y * 10
        return function(z){
          return x + y + z
        }
      }
    }
    function zhCurrying(fn){
        function curried(...args) {
        //判断当前已经接收的参数的个数,如果柯里化函数接收的参数和函数调用的参数的个数一致了,就调用函数
            if(args.length >= fn.length){
                return fn.apply(this,args)
            }else {
                //没有达到个数时,需要返回一个新的函数,继续来接收参数
                function curried2(...arg2) {
                    //接收到参数后,需要递归调用curried来检查函数的个数是否达到
                    return curried.apply(this,args.concat(args2))
                }
                return curried2
            }
        }
        return curried
    }
    var curryAdd = hyCurrying(add1);
    console.log(curryAdd(10, 20, 30));
    console.log(curryAdd(10, 20)(30));
    console.log(curryAdd(10)(20)(30));

柯里化其实用到了闭包的知识,一个一个的参数处理逻辑,其实是放在fn函数中的,但是在柯里化的过程中,由于闭包的功能,我们能够在每层的函数处理过程中都能拿到捕捉到的变量,最终的逻辑处理还是放在fn函数中处理。在我们对函数操作的逻辑中,只有拿到对应的参数才会执行到对应的参数处理逻辑,所以也相当于对对应的逻辑进行了封装。

组合函数

组合函数时在JavaScript开发过程中一种对函数的使用技巧和模式。

比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1,fn2,这两个函数是依次执行的;

那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复。

那么是否可以将这两个函数组合起来,自动一次调用呢?

这个过程就是对函数的组合,我们称之为组合函数。

组合函数案例

//比如有如下两个函数
    function double(num) {
        return num * 2
    }
    function square(num) {
        return num ** 2
    }
    //一般的调用方式
    var count = 10
    var result = square(double(count))
    console.log(result)

    //组合函数实现
    function composeFn(m,n){
        return function(count) {
            reutrn n(m(count))
        }
    }
    // 组合函数的调用方式
    var newFn = composeFn(double, square)
    console.log(newFn(10))

通用组合函数的编写

    function zhCompose(...fns){
        var length = fns.length
        for(var i = 0; i < length;i++){
            if(typeof fns[i] !== 'function'){
            
                throw new TypeError('Expected arguments are functions')
            }
        
        }
        
        function compose(...args) {
            var index = 0
            var result = length?fns[index].apply(this,args):args
            while(++index < length) {
                result = fns[index].call(this,result)
            }
            return result
        }
        return compose
    
    }
    
    function double(m) {
      return m * 2
    }

    function square(n) {
      return n ** 2
    }

    var newFn = hyCompose(double, square)
    console.log(newFn(10))