• JS之函数式编程


    函数式编程的思想手写一些常用api方法

    • 手写forEach
    const forEach = (arr, fn) => {
      for (let i of arr) {
        fn(i)
      }
    }
    
    • 手写filter
    const filter = (arr, fn) => {
      let lsArr = []
      for(let i of arr) {
        if (fn[i]) lsArr.push(i)
      }
      return lsArr
    }
    
    • 手写once (比如支付,点击一次只会执行一次函数--利用闭包)
    const once = (fn) => {
      let status = true
      return function() {
        if (status) {
          status = false
          return fn.apply(this, arguments)
        }
      }
    }
    let pay = once(function(m) {
      console.log(`这是付的钱${m}`)
    })
    pay(1)
    
    • 手写map
    function map (arr, fn) {
      let lsArr = []
      for (var i = 0; i < arr.length; i++) {
        lsArr.push(fn(arr[i]))
      }
      return lsArr
    }
    
    • 手写every(只要有一个不匹配就返回false)
    const every = (arr, fn) => {
      let status = true
      for (let i of arr) {
        status = fn(i)
        if (!status) break
      }
      return status
    }
    
    • 手写some(只要有一个匹配到就返回true)
    const some = (arr, fn) => {
      let status = false
      for (let i of arr) {
        status = fn(i)
        if (status) break
      }
      return status
    }
    
    • 手写lodash的记忆函数memoize(对执行同一个的纯函数做缓存处理)
    const memoize = (fn) => {
      let lsObj = {}
      return function() {
        let key = JSON.stringify(fn)
        lsObj[key] = lsObj[key] || fn.apply(fn, arguments)
        return lsObj[key]
      }
    }
    
    • 纯函数:对同一个函数多次执行,输出的结果相同

      • 副作用:比如在函数中用到了全局变量,或者是配置文件,数据库,用户输入等,都降低方法的可重用性,同时也会带来不安全的隐患
      • 同时,副作用不可避免
    • 闭包:在另一个作用域可以调用函数内部作用域的属性

      本质: 函数在执行时会被放到一个执行栈上,函数执行完毕会从执行栈上移除,但是如果存在外部引用,那么堆上的作用域成员就不会别释放,所以闭包的过程中内部函数依旧可以访问外部函数的成员

    对柯里化的一些理解

    一等公民

    • 函数也是一个对象,我们可以把函数作为一个值去处理,也就是高阶函数

    柯里化

    • 柯里化概念:当一个函数有多个参数的时候,我们可以对其进行改造,可以只接受部分参数,然后return一个新函数,去接收剩余的参数,然后在返回结果,这就是函数柯里化
    function checkAge(min) {
      return function(age) {
        return age >=min
      }
    }
    let checkAge18 = checkAge(18)
    checkAge18(20) // 此时min基数为18,age为20
    // 改造
    const checkAge = min => age => age >= min
    

    lodash中curry的使用(lodash中的柯里化函数)

    • curry的基本使用

    可以传入一个纯函数,然后如果传入的参数是这个纯函数的所有参数,直接执行这个函数,如果传入的不是全部参数,则返回该函数并等待接收剩下的参数

    const getSum = (a, b, c) => return a + b + c
    const _ = require('lodash')
    const curried = _.curry(getSum)
    curried(1, 2, 3) // 6
    curried(1, 2)(3) // 6
    curried(1)(2, 3) // 6
    

    手写lodash中的curry

    const curry = (fn) => {
      // 通过展开运算符...args,来将传入的参数展开,那么args也就是放着所有的参数的数组
      return function curriedFun(...args) {
    		//可以通过形参fn.length 来拿到fn函数的形参长度值
        if (args.length < fn.length) {
          // 传入的参数不全,返回一个新函数,直至传入的该函数的全部参数
          return function() {
    				return curriedFun(...args.concat(Array.from(arguments)))
          }
        }
        // 此时,传入的参数是所有参数
        return fn(...args)
    	}
    }
    
    • 总结

      • 柯里化可以让我们生成一个拥有固定参数的新函数
      • 相当于对函数进行了一次缓存
      • 降低了函数的粒度
      • 将多元函数转换成一元函数
    • 案例1(字符串的match方法,正则表达式可能是不经常换的,所以可以进行柯里化处理)

    ''.match(/s+/g)
    const match = _.curry(function(reg, str) {
      return str.match(reg)
    })
    haveSpace = match(/s+/g)
    hanvSpace('') // 也就实现了第一行的一个重用性封装
    
    • 案例2(数组的过滤方法,过滤规则函数可能不是经常换的)
    const filter = _.curry(function(arr, fn) {
      return arrr.filter(fn)
    })
    const filterSpace = filter(haveSpace)
    

    函数组合以及函子

    函数组合(compose)

    • 如果一个函数的执行要经过多个函数的处理才能得到最终结果,可以把中间的多个函数组合成一个函数
      • 也就是把数据管道从一大拆多小
      • 函数组合默认是从右往左执行的
    function reverse(arr) {
      return _.reverse(arr)
    }
    function first(arr) {
      return arr[0]
    }
    // 将reverse函数和first函数组合起来
    function compose(a, b) {
      return function(value) {
        return a(b(value))
      }
    }
    
    const c = compose(first, reverse)
    console.log(c([1, 2, 3, 4])) // 输出结果: 4
    
    • lodash提供了两个组合函数
      • flowRight (从右往左执行函数)
      • flow (从左往右执行函数)
    • 手写组合函数
    const compose = (...args) => (val) => args.reverse().reduce((prev, fn) => fn(prev), val)
    

    组合函数的结合律

    const one = _.flowRight(_.toUpper, _.first, _.reverse)
    const two = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
    const three = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
    
    • 案例一 "HELLO WORLD" -> "hello-world"
    // 因为flowRight只能传接受一个参数的函数,故给其柯里化, 下面同此
    const split = _.curry((sep, str) => _.split(str, sep) 
    const map = _.curry((fn, arr) => _.map(arr, fn))
    const join = _.curry((sep, arr) => _.join(arr, sep))
    const log = _.curry((tag, v) => {
      console.log(tab, v)
      return v
    })
    const c = _.flowRight(join('-'), log('map后'), map(_.toLower), log('map前'), split(' '))
    

    lodash中fp模块

    • 因lodash中的像map,split,join等还需要我们额外柯里化一下,所以可以使用lodash提供的fp模块
    const fp = require('lodash/fp')
    const c = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
    console.log(c("HELLO WORLD")) // 输出与案例一相同
    

    fp模块的map和lodash普通的_.map的区别

    • lodash中_.map需要传入的参数为arr, fn, fn接到的参数是arr的每一个值,索引,arr本身,所以在假如把数组中的所有数字字符串变数组时就会出现问题
    const a = ['1', '2', '3']
    console.log(_.map(a, parseInt))
    // 会输出[1, NAN, NAN]
    /*
    	因为在这里parseInt接到的参数是
    	parseInt('1', 0, arr)	
    	parseInt('2', 1, arr)
    	parseInt('3', 2, arr)
    	parseInt可以接收两个参数,第一个参数是要操作的值,第二个参数是操作成几进制
    	所以数组中第二个数转变成1进制,没有,则为NAN,
    */
    
    • fp中的map则是第一个参数为fn,第二个参数为arr,所以fn接收的就是arr里每个值
    const a = ["1", "2", "3"]
    console.log(fp.map(parseInt, a))
    // 输出[1, 2, 3]
    

    point free

    • point free也就是函数组合compose
    • 案例
    const fp = require('lodash/fp')
    const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.toUpper, fp.first)), fp.split(' '), fp.replace(/s+/g, ' '))
    console.log(firstLetterToUpper("HELLO  WORLD WWW"))
    

    函子(functor)

    • 函子也就是functor,它是一个容器,也就是一个对象
    • 主要是为了处理副作用
    • 函子是一个具有map方法的一个对象,在函子里有一个要维护的值,但是这个值不对外公布,如果要对这个值进行处理的话,需要调用map方法,map方法接收一个处理值的函数,执行完毕返回一个新的函子,也就实现了链式调用
    class Container {
      // 定义静态方法可以直接访问
      static of(value) {
        return new Container(value)
    	}
      constructor(value) {
        this._value = value
    	}
      map(fn) {
        return Container.of(fn(this._value)
    	}
    }
    let r = Container.of(5).map(x => x + 2)  // 此时函子(容器)中this._value = 7
    

    总结

    • 函数式编程的运算不直接操作值,而是由函子来完成的
    • 函子是一个实现了map契约的对象
    • 我们可以把函子看成一个盒子,这个盒子里封装着一个值
    • 想要处理盒子的值,只能通过map方法,去传递进去一个纯函数,让这个纯函数对值进行处理
    • 最终map方法返回了一个包含新值的函子(盒子)

    MayBe函子

    • 如上函子,当我们传入一个null或者是undefined,此时我们在map中另其做一些操作,比如xx.toUpperCase()就会出现报错,所以我们可以采用MayBe函子的形式来进行处理
    class MayBe {
    	static of(value) {
        return new MayBe(value)
      }
      constructor(value) {
        this._value = value
      }
      map(fn) {
        return this.isNothing ? MayBe.of(null) : MayBe.of(fn(this._value))
      }
    	isNothing() {
        return this._value === null || this._value === undefined
    	}
    }	
    

    但是这样如果多个地方返回undefined或者是null的话就无法准确定位到是哪返回的

    Either函子

    • Either函子更像是我们的if...else...
    • 通过try catch以及定义两个函子的方式来进行对的时候执行Right,错误的时候抛出到Left函子
    class Left {
      static of (value) {
        return new Left(value)
      }
      constructor(value) {
        this._value = value
      }
      map() {
        return this
      }
    }
    class Right {
      static of (value) {
        return new Right(value)
      }
      constructor(value) {
        this._value = value
      }
      map(fn) {
        return Right.of(fn(this._value))
      }
    }
    // 测试定义一个函数
    const parseJSON = (str) => {
      try {
        // 传入参数正确时会走Right函子
    		return Right.of(JSON.parse(str))
      } catch (err) {
        // 反之传入参数不正确走Left函子
        return Left.of({ error: err.message })
      }
    }
    

    IO函子

    • IO函子中的_value是一个函数,把函数作为值来处理
    • 这样,就可以把不纯的函数存储到_value直接OMG,延迟执行这个函数(惰性执行)
    • 把不纯的函数交给调用者执行
    const fp = require('lodash/fp')
    class IO {
      // 再of静态方法中传入一个函数,但是还是为了拿到一个值
      static of (x) {
        return new IO(function() {
    			return x
        })
    	}
      constructor(fn) {
        this._value = fn
      }
      // 调用map,返回一个新的IO函子,并将this._value和传入map的参数合成一个函数
      map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    	}
    }
    

    folktale

    • folktale 是一个标准的函数式编程库
    • 提供了像compose,curry的函数处理
    • 提供了MayBe, Task, Either等函子

    folktale中和lodash中相似的功能

    • compose - flowRight (这个基本一样)
    const { toUpper, first, flowRight } = require('lodash/fp')
    const { compose } = require('folktale/core/lambda')
    let f = compose(toUpper, first)
    let lF = flowRight(toUpper, first)
    console.log(f(['hello', 'world']))
    console.log(lF(['hello', 'world']))
    
    • 两边curry的区别
    const { toUpper, first, curry } = require('lodash/fp')
    const { curry } = require('folktale/core/lambda')
    // 第一个参数为参数的个数
    let f = curry(2, (x, y) => x + y)
    let lF = curry((x, y) => x + y)
    

    Task函子

    Pointed函子

    • 这个函子也就是前面在普通函子中定义的of静态方法,主要是通过of方法来把值放到上下文Context中,也就是把值放到容器中,然后再通过map来进行处理

    Monad函子

  • 相关阅读:
    ToString 格式化数值
    肾积水
    十月一日
    9月27日 星期六
    080929 气温骤降
    東京の空
    9月26日 星期五
    9月30日 星期二
    粉蓝房子&电影
    080922 雨
  • 原文地址:https://www.cnblogs.com/Huskie-/p/14684837.html
Copyright © 2020-2023  润新知