函数式编程

95 阅读3分钟

为什么学习函数式编程

  • 函数式编程是随着React的流行受到越来越多的关注
  • vue3也开始拥抱函数式编程
  • 函数式编程可以抛弃this
  • 打包过程可以更好的利用tree shaking过滤无用代码
  • 方便测试、方便并行处理
  • 很多库可以帮助我们进行函数式开发:laodash、underscore、ramda

什么是函数式编程

  1. 函数式编程的思维:把现实世界的事务和事务之间的联系抽象到程序世界(对运算过程进行抽象)
  2. 函数式编程中的函数指的是数学中的函数,相同的输入始终得到相同的输出,例如:y = sin(x),x和y的关系
// 函数式
function add (n1, n2) {
  return n1 + n2
}
let sum = add(2, 3)

函数式一等公民

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数作为返回值

高阶函数

  1. 什么是高阶函数
  • 可以把函数作为参数传递给另一个函数
  • 可以把函数作为另一个函数的返回结果
// 函数作为参数
function forEach(array, fn) {
  for (let i = 0; i < array.length; i++) {
    fn(array[i])
  }
}

const arr = [1, 3, 5, 7, 9]
forEach(arr, function(item)) {
  console.log(item)
})
// 函数作为返回值
function once(fn) {
 let done = false
 return function() {
   if(!done) {
    done = true
    return fn.apply(this, arguments)
   }
 }
}

let pay = once(function (money) {
  console.log(`支付:` ${money} RMB)
})

使用高阶函数的意义

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

闭包-概念

  • 闭包:函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
  • 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问到外部函数的成员

闭包-案例

  function makePower(power) {
    return function(number) {
      return Math.pow(number, power)
    }
  }
  
  let power2 = makePower(2)
  console.log(power2(4)) // 16
  function makeSalart(base) {
    return function(performance) {
      return base + performance
    }
  }
  
  let salaryLevel1 = makeSalary(14000)
  console.log(salaryLevel1(2000)) // 16000

纯函数

  1. 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  2. 纯函数的代表:Lodash
  3. 纯函数的好处
  • 可缓存因为纯函数相同的输入永远会得到相同的输出,所以可以把纯函数的结果缓存起来
  • 可测试
    1. 纯函数让测试更方便
  • 并行处理
    1. 在多线程环境下并行操作共享的内存数据可能会出现意外情况
    2. 纯函数不需要访问共享的内存数据,所以在并行环境下可以人员运行纯函数(Web Worker)

函数的副作用

  1. 副作用让一个函数变的不纯,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
// 非纯函数
let mini = 18
function checkAge(age) {
  return age >= mini
}
  1. 副作用来源: 配置文件、数据库、获取用户的输入......
  2. 所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合拓展和可重用性,同时副作用会给程序带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。

柯里化

  1. 概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果
 function checkAge(age) {
   let mini = 18 // 硬编码(写死)
   return age >= mini
 }
 
 // 柯里化
 function checkAge(min) {
   return function(age) {
     return min >= age
   }
 }
 // 柯里化ES6写法
 let checkAge = min => (age => min >= age)
 
 let checkAge18 = checkAge(18)
 checkAge(24)
  1. 柯里化案例
// lodash中的柯里化函数
const _ = require('lodash')
function getSum(a, b, c) {
  return a + b + c
}

const curried = _.curry(getSum)

console.log(curried(1, 2, 3)) // 6
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6
  1. 模拟lodash柯里化函数
function curry(func) {
  return function curriedFn(...args) {
    // 判断实参和行参的个数
    if (args.length < func.length) {
      return function() {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}
  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的'缓存'
  • 让函数变的更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合

  • 函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
    • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果函数组合默认是从右到左执行
  // 函数组合演示(获取数组最后一个元素)
  function compose(f, g) {
    return function(value) {
     return f(g(value))
    }
  }
  
  function revese(array) {
    return array.revese()
  }
  
  function first(array) {
    return array[0]
  }
  
  const last = compose(firse, reverse)
  
  consol.log(last[1, 2, 3, 4])  // 4
  • lodash 中的组合函数
  • lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数,flow() 是从左到右运行 flowRight() 是从右到左运行,使用的更多一些
const _ = require('lodash')

const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
// 获取数组中最后一个元素并转换成大写
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three'])) // THREE
  • 模拟实现 lodash 的 flowRight 方法
function compose(...args) {
  return function(value) {
    return args.reverse().reduce(function(acc, fn) { // 此处的fn就是数组中的每一个元素
      return fn(acc)
    }, value)
  }
}

// ES6写法
const comopse = (...args) => value => args.reverse().reduce((acc,fn) => fn(acc), value)
// reduce()方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
  • 函数的组合要满足结合律 (associativity)
    • 我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
    // 结合律(associativity) 
    let f = compose(f, g, h)
    let associative = compose(compose(f, g), h) == compose(f, compose(g, h)) // true
    

Functor (函子)

为什么要学函子

  • 到目前为止已经已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用 控制在可控的范围内、异常处理、异步操作等。

什么是 Functor

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)