函数式编程


  • 函数式编程概念

函数式编程是编程范式之一

思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

其中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系

相同的输入始终要得到相同的输出


  • 为什么要学函数式编程

  1. React vue3都越来越多使用函数式编程

  2. 函数式编程可以抛弃this

  3. 打包过程中可以更好的利用tree shaking 过滤无用代码

  4. 方便测试、方便并行利用

  5. lodash、underscore、ramda等库可以进行函数式开发
    ```javascript //非函数式 let num1 = 2 let num2 = 3 let sum = num1 + num2

//函数式 function add(n1, n2) { return n1 + n2 } let sum = add(2, 3)

  1. - <br />函数可以存储在变量中<br />
  2. ```javascript
  3. let fn = function() {
  4. console.log('hello');
  5. }
  6. fn()
  7. //示例
  8. const Controller = {
  9. index (posts) { return Views.index(posts) }
  10. }
  11. //优化
  12. const Controller = {
  13. index: Views.index
  14. }

  • 函数可作为参数
    ```javascript function forEach(arr, fun) { for(let i =0; i < arr.length; i++) {
    1. fun(arr[i])
    } }

let arr = [1,2,3,4] forEach(arr, function(item) { console.log(item) })

  1. - <br />函数作为返回值<br />
  2. ```javascript
  3. function once(fun) {
  4. let done = false
  5. return function() {
  6. if(!done) {
  7. done = true;
  8. return fun.apply(this, arguments)
  9. }
  10. }
  11. }
  12. let pay = once(function(money) {
  13. console.log(${money});
  14. })
  15. pay(3)
  16. pay(3)

  • 使用高阶函数的意义
    • 抽象可以帮我们屏蔽细节,只需要关注我们的目标
    • 高阶函数是用来抽象通用的问题,即可复用

  • 常用的高阶函数

forEach map filter every some find/findIndex reduce sort

  • 模拟map
  1. const map = (arr, fun) => {
  2. let res = [];
  3. for(let value of arr) {
  4. res.push(fun(value));
  5. }
  6. return res;
  7. }
  8. let arr = [1,2,3,4]
  9. arr = map(arr, v => v*v)
  10. console.log(arr);

  • 闭包
    • 概念: 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
      • 即可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
  1. function mfn() {
  2. let msg = 'Hello';
  3. return function() {
  4. console.log(msg);
  5. }
  6. }
  7. const fn = mfn();
  8. fn()
  9. //即在fn的作用域中,调用了mfn函数的内部函数,并访问了msg
  • 本质: 函数在执行的时候,会放到一个执行栈上,当函数执行完毕后会从执行栈上移除,但堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数成员

  • 闭包示例

这里优化后,求一个数的多少次方,进行了一个封装,传参变为一个

下面在浏览器中调试,可以查看发生闭包的整个流程:

(Call Stack 函数的调用栈,Scope 作用域,Closure 闭包)

函数式编程 - 图1

  1. // Math.pow(4, 2)
  2. // Math.pow(5, 2)
  3. function makePower (power) {
  4. return function (number) {
  5. return Math.pow(number, power)
  6. }
  7. }
  8. // 求平方
  9. let power2 = makePower(2)
  10. let power3 = makePower(3)
  11. console.log(power2(4))
  12. console.log(power3(5))

  • 纯函数: 相同的输入永远会得到相同的输出

举例:

  • slice 纯函数,返回指定部分,不会改变原数组
  • splice 不纯函数,操作数组,改变原数组

函数式编程不会保留计算中间的结果,所以变量是不可变的


  • 纯函数好处

    • 可缓存,可提高程序的性能,因为计算是需要花费时间的
      ```javascript // 记忆函数 const _ = require(‘lodash’)

function getArea (r) { console.log(r) return Math.PI r r }

// let getAreaWithMemory = _.memoize(getArea) // console.log(getAreaWithMemory(4)) // console.log(getAreaWithMemory(4)) // console.log(getAreaWithMemory(4))

// 模拟 memoize 方法的实现 function memoize (f) { let cache = {} return function () { let key = JSON.stringify(arguments) //以下判断以arguments参数为键值的,在cache缓存里有没有,有就直接返回缓存,没有就重新计算 cache[key] = cache[key] || f.apply(f, arguments) return cache[key] } }

let getAreaWithMemory = memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4))

  1. - 可测试,纯函数始终会有输入和输出
  2. - 并行处理
  3. - 在多线程环境下并行操作共享的内存数据可能会出现意外情况
  4. - 而纯函数不需要访问共享的内存数据,在并行环境下也可以任意运行纯函数
  5. - <br />副作用<br />
  6. ```javascript
  7. //不纯
  8. let mini = 18;
  9. function chechAge(age) {
  10. return age >= mini
  11. }
  12. //纯 即把mini放进函数里
  • 副作用
    • 不纯函数,依赖于外部的状态,就无法保证输出的相同
  • 来源
    • 配置文件
    • 数据库
    • 获取用户的输入 等

  • 柯里化

当函数有多个参数的时候,对这个函数进行改造,调用一个函数只传递部分的参数,并且让这个函数返回一个新的函数,让新的函数去接收剩余的参数,并且返回相应的结果,这就是函数的柯里化。

  1. // 柯里化演示
  2. //虽然是纯函数,但里面有硬编码
  3. // function checkAge (age) {
  4. // let min = 18
  5. // return age >= min
  6. // }
  7. // 普通的纯函数
  8. // function checkAge (min, age) {
  9. // return age >= min
  10. // }
  11. //里面有多个参数,min基准值会重复调用
  12. // console.log(checkAge(18, 20))
  13. // console.log(checkAge(18, 24))
  14. // console.log(checkAge(22, 24))
  15. // 函数的柯里化
  16. // function checkAge (min) {
  17. // return function (age) {
  18. // return age >= min
  19. // }
  20. // }
  21. // ES6写法
  22. let checkAge = min => (age => age >= min)
  23. let checkAge18 = checkAge(18)
  24. let checkAge20 = checkAge(20)
  25. console.log(checkAge18(20))
  26. console.log(checkAge18(24))

  • Lodash中的柯里化方法
    • _.curry(func)
      • func即需要柯里化的函数
      • 返回柯里化后的函数,参数需要跟func函数数量一样
  1. // lodash 中的 curry 基本使用
  2. const _ = require('lodash')
  3. function getSum (a, b, c) {
  4. return a + b + c
  5. }
  6. const curried = _.curry(getSum)
  7. console.log(curried(1, 2, 3))
  8. console.log(curried(1)(2, 3))
  9. console.log(curried(1, 2)(3))
  10. console.log(curried(1)(2)(3))

  • 柯里化案例
    ```javascript // 柯里化案例 // ‘’.match(/\s+/g)

const _ = require(‘lodash’)

const match = _.curry(function (reg, str) { return str.match(reg) })

const haveSpace = match(/\s+/g)

const filter = _.curry(function (func, array) { return array.filter(func) }) const findSpace = filter(haveSpace)

// console.log(haveSpace(‘helloworld’)) // console.log(haveNumber(‘abc’))

console.log(filter(haveSpace, [‘John Connor’, ‘John_Donne’]))

console.log(findSpace([‘John Connor’, ‘John_Donne’])) //最终优化成,可以判断一个数组的参数,并调用一个函数

  1. - <br />柯里化原理模拟<br />
  2. ```javascript
  3. //函数形式写完
  4. function getSum (a, b, c) {
  5. return a + b + c
  6. }
  7. const curried = curry(getSum)
  8. //如何调用函数
  9. console.log(curried(1, 2, 3))
  10. console.log(curried(1)(2, 3))
  11. console.log(curried(1, 2)(3))
  12. function curry (func) {
  13. return function curriedFn(...args) {
  14. // 判断实参和形参的个数
  15. if (args.length < func.length) {
  16. // 如果只传部分函数,就返回一个函数
  17. //把之前的参数跟剩余的参数合并到一起,调用原来那个参数
  18. return function () {
  19. return curriedFn(...args.concat(Array.from(arguments)))
  20. }
  21. }
  22. //如果实形数量相同,直接调用原柯里化的函数,并把参数都传进去
  23. return func(...args)
  24. }
  25. }

  • 柯里化总结
    • 柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数
    • 这是一种对函数参数的‘缓存’
    • 让函数变得更灵活,让函数的粒度更小
    • 可以把多元函数转换成一元函数,可以组合使用函数产生强在的功能

  • 函数组合

    • 洋葱代码: .toUpper(.first(_.reverse(array)));
    • 函数组合(compose): 如果一个函数要经过多个函数处理才能得到最终值,就可以把中间过程的函数合并成一个函数
    • 函数组合默认是从右到左执行
    • 函数组合满足结合律,先合哪执行,结果一样

      1. fn = compose(f1, f2, f3)
      2. b = fn(a);
      • 示例
  1. // 函数组合演示
  2. function compose (f, g) {
  3. return function (value) {
  4. return f(g(value))
  5. }
  6. }
  7. function reverse (array) {
  8. return array.reverse()
  9. }
  10. function first (array) {
  11. return array[0]
  12. }
  13. const last = compose(first, reverse)
  14. console.log(last([1, 2, 3, 4]))

  • Lodash中的组合函数
    • flow() 从左至右运行
    • flowRight() 从右至左运行,使用更多
  1. // lodash 中的函数组合的方法 _.flowRight()
  2. const _ = require('lodash')
  3. const reverse = arr => arr.reverse()
  4. const first = arr => arr[0]
  5. const toUpper = s => s.toUpperCase()
  6. const f = _.flowRight(toUpper, first, reverse)
  7. console.log(f(['one', 'two', 'three']))

  • 组合函数原理模拟
    ```javascript // 模拟 lodash 中的 flowRight

const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase()

  1. //参数多少个不确定,所以用 ...args

// function compose (…args) { //_.flowRight需要返回一个函数,并需要对一个参数做处理,所以返回一个函数并传进去参数value // return function (value) { //.reduce()对数组中的每一个元素,去执行我们提供的一个函数 // return args.reverse().reduce(function (acc, fn) { //acc,是累计的结果,然后累计的结果每次执行fn函数 // return fn(acc) //acc,初始值是value // }, value) // } // }

const compose = (…args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

const f = compose(toUpper, first, reverse) console.log(f([‘one’, ‘two’, ‘three’]))

  1. - <br />函数式编程里面,会经常使用箭头函数,因为会使代码更简洁
  2. - <br />函数组合调试<br />
  3. ```javascript
  4. //辅助调试函数
  5. const trace = _.curry((tag, v) => {
  6. console.log(tag, v)
  7. return v
  8. })
  9. const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))

  • Lodash-fp

fp提供了对我们函数式编程友好的方法,fp模块中提供的方法,都是已经被柯里化的,而且有多个参数的话,都是函数优先,数据置后,而lodash _.里面都是数据优先函数置后

示例

  1. // 不使用fp
  2. const split = _.curry((sep, str) => _.split(str, sep))
  3. const join = _.curry((sep, array) => _.join(array, sep))
  4. const map = _.curry((fn, array) => _.map(array, fn))
  5. const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))
  6. //使用fp
  7. const fp = require('lodash/fp')
  8. const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))

  • Lodash-map方法的小问题
    ```javascript // lodash 和 lodash/fp 模块中 map 方法的区别 // const _ = require(‘lodash’)

    // console.log(_.map([‘23’, ‘8’, ‘10’], parseInt)) // // parseInt(‘23’, 0, array) // // parseInt(‘8’, 1, array) // // parseInt(‘10’, 2, array)

  1. const fp = require('lodash/fp')
  2. console.log(fp.map(parseInt, ['23', '8', '10']))
  1. - <br />Point Free 是一种编程风格
  2. - 不需要指明处理的数据
  3. - 只需要合成运算过程
  4. - 需要定义一些辅助的基本运算函数
  5. ```javascript
  6. const fp = require('lodash/fp')
  7. const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
  8. console.log(f('Hello World'))

  • Functor 函子
    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子就是一个实现了map契约的对象
    • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
    • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
    • 最终map方法返回一个包含新值的盒子(函子)
  1. // // Functor 函子
  2. // class Container {
  3. // constructor (value) {
  4. // this._value = value
  5. // }
  6. // map (fn) {
  7. // return new Container(fn(this._value))
  8. // }
  9. // }
  10. // let r = new Container(5)
  11. // .map(x => x + 1)
  12. // .map(x => x * x)
  13. // console.log(r)
  14. class Container {
  15. static of (value) {
  16. return new Container(value)
  17. }
  18. constructor (value) {
  19. this._value = value
  20. }
  21. map (fn) {
  22. return Container.of(fn(this._value))
  23. }
  24. }
  25. //这里有问题,传值可能为null的风险
  26. let r = Container.of(5)
  27. .map(x => x + 2)
  28. .map(x => x * x)
  29. console.log(r)

  • MayBe函子

如果传入的值为null或者undefined,会报错

所以这个函数变得不纯,这也是副作用

  1. // 演示 null undefined 的问题
  2. Container.of(null)
  3. .map(x => x.toUpperCase())
  1. // MayBe 函子
  2. class MayBe {
  3. //外部要访问MayBe函子的话,需要去new 这个类,为了外部创建MayBe函子的时候更方便一些,我们给他设置一个静态的方法,new的过程就封装到了of方法里面,初始化这个构造函数
  4. static of (value) {
  5. return new MayBe(value)
  6. }
  7. //初始化一个构造函数,接收一个值,设置一个属性接收这个值,这个value是不希望外部访问的
  8. constructor (value) {
  9. this._value = value
  10. }
  11. //map方法,接收一个函数,来处理这个值,并返回一个新的函子,返回的结果作为这个函数新的参数,这里如果传起来的值为null,直接返回一个值为null的函子,不应该调用fn,可能会出现危险
  12. map (fn) {
  13. return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  14. }
  15. isNothing () {
  16. return this._value === null || this._value === undefined
  17. }
  18. }
  19. // let r = MayBe.of(null)
  20. // .map(x => x.toUpperCase())
  21. // console.log(r)
  22. //会打印出一个值为null的函子:
  23. // MayBe { _value: null }
  24. let r = MayBe.of('hello world')
  25. .map(x => x.toUpperCase())
  26. .map(x => null)
  27. .map(x => x.split(' '))
  28. console.log(r)
  29. //MayBe函子的问题是,虽然我们可以控制null的传值,但多次调用map方法的时候,不知道是什么时候传的null
  • MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围内)

  • Either 函子
    • 类似if…else…
    • 异常会让函数变的不纯,Either函子可以用来做异常处理
  1. // Either 函子
  2. class Left {
  3. static of (value) {
  4. return new Left(value)
  5. }
  6. constructor (value) {
  7. this._value = value
  8. }
  9. map (fn) {
  10. return this
  11. }
  12. }
  13. class Right {
  14. static of (value) {
  15. return new Right(value)
  16. }
  17. constructor (value) {
  18. this._value = value
  19. }
  20. map (fn) {
  21. return Right.of(fn(this._value))
  22. }
  23. }
  24. // let r1 = Right.of(12).map(x => x + 2)
  25. // let r2 = Left.of(12).map(x => x + 2)
  26. // console.log(r1)
  27. // console.log(r2)
  28. function parseJSON (str) {
  29. try {
  30. return Right.of(JSON.parse(str))
  31. } catch (e) {
  32. return Left.of({ error: e.message })
  33. }
  34. }
  35. //错误会进入Left,显示错误信息
  36. // let r = parseJSON('{ name: zs }')
  37. // console.log(r)
  38. let r = parseJSON('{ "name": "zs" }')
  39. .map(x => x.name.toUpperCase())
  40. console.log(r)

  • IO 函子 输入输出
    • IO 函子中的_value是一个函数,这里是把函数作为值来处理
    • IO 函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
    • 把不纯的操作交给调用者来处理
  1. // IO 函子
  2. const fp = require('lodash/fp')
  3. class IO {
  4. static of (value) {
  5. return new IO(function () {
  6. return value
  7. })
  8. }
  9. constructor (fn) {
  10. this._value = fn
  11. }
  12. map (fn) {
  13. return new IO(fp.flowRight(fn, this._value))
  14. }
  15. }
  16. // 调用
  17. let r = IO.of(process).map(p => p.execPath)
  18. // console.log(r)
  19. console.log(r._value())

  • Folktale

    • 一个标准的函数式编程库

    • 没有提供很多功能函数

    • 但提供了一些函数式处理的操作,如: compose curry等,一些函子如 Task Either Maybe等

    • 异步任务实现过于复杂,我们使用foletale中的Task来演示
  1. // folktale 中的 compose、curry
  2. const { compose, curry } = require('folktale/core/lambda')
  3. const { toUpper, first } = require('lodash/fp')
  4. //柯里化,这里多传了一个参数2,意味着传入两个参数
  5. // let f = curry(2, (x, y) => {
  6. // return x + y
  7. // })
  8. // console.log(f(1, 2))
  9. // console.log(f(1)(2))
  10. //函数组合
  11. let f = compose(toUpper, first)
  12. console.log(f(['one', 'two']))

  • Task 函子处理异步任务

    1. // Task 处理异步任务
    2. const fs = require('fs')
    3. const { task } = require('folktale/concurrency/task')
    4. const { split, find } = require('lodash/fp')
    5. function readFile (filename) {
    6. return task(resolver => {
    7. //filename 文件路径,读取文件
    8. fs.readFile(filename, 'utf-8', (err, data) => {
    9. //失败
    10. if (err) resolver.reject(err)
    11. //成功
    12. resolver.resolve(data)
    13. })
    14. })
    15. }
    16. //当前目录下读书文件
    17. readFile('package.json')
    18. //函子都会有map方法,在这里处理我们拿到的结果
    19. //package.json里面都是一行一行的,我们把他们以行分隔成数组,然后找到包含'version'的那个元素
    20. .map(split('\n'))
    21. .map(find(x => x.includes('version')))
    22. .run() //函子提供的run()方法读取文件
    23. //函子提供的listen()监听的方法
    24. .listen({
    25. //失败
    26. onRejected: err => {
    27. console.log(err)
    28. },
    29. //成功
    30. onResolved: value => {
    31. console.log(value)
    32. }
    33. })
    34. //执行结果
    35. //"version": "1.0.0",

  • Pointed 函子

    • 是实现了of静态方法的函子
    • 之前的函子都有of方法,所以都是Pointed函子,of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文context(把值放到容器中,使用map来处理值)

  • Monad 函子
    • IO函子的问题,调用嵌套函子中的函数的时候不方便
  1. // IO(IO(x))
  2. let r = cat('package.json')._value()._value()
  • Monad 函子是可以变扁的Pointed函子,变扁就是解决嵌套的问题
  • 一个函子如果具有join和of两个方法,并遵守一些定律,就是一个Monad
  1. // IO Monad
  2. const fs = require('fs')
  3. const fp = require('lodash/fp')
  4. class IO {
  5. static of (value) {
  6. return new IO(function () {
  7. return value
  8. })
  9. }
  10. constructor (fn) {
  11. this._value = fn
  12. }
  13. map (fn) {
  14. return new IO(fp.flowRight(fn, this._value))
  15. }
  16. join () {
  17. return this._value()
  18. }
  19. //flatMap的作用就是把map和join一起用,变扁平
  20. flatMap (fn) {
  21. return this.map(fn).join()
  22. }
  23. }
  24. //虽然依赖外部文件,但当前的操作是纯的
  25. let readFile = function (filename) {
  26. return new IO(function () {
  27. return fs.readFileSync(filename, 'utf-8')
  28. })
  29. }
  30. let print = function (x) {
  31. return new IO(function () {
  32. console.log(x)
  33. return x
  34. })
  35. }
  36. let r = readFile('package.json')
  37. // .map(x => x.toUpperCase())
  38. .map(fp.toUpper)
  39. .flatMap(print)
  40. .join()
  41. console.log(r)