1.为什么要学习函数式编程
- 函数式编程是随着React的流行收到越来越多的关注
- Vue 3 也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的利用tree shaking过滤无用代码
- 方便测试、方便并行处理
- 很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
2.函数式编程的定义
(functional programming)简称FP,函数式编程就是把程序抽象到一起,做数据(函数)之间的映射,本质上是通过某种输入获得某种输出。
纯函数:相同的输入要得到相同的输出
// 非函数式
let num1 = 1
let num2 = 2
let sum = num1 + num2
// 函数式
function add (n1, n2) {
return n1 + n2
}
let sum = add(num1, num2)
2.函数的作用
- 函数式一等公民
- 闭包
- 函数柯里化
2.1 高阶函数(Higher Order Component)
- 高阶函数可以作为参数传递给另外一个函数
- 可以把函数作为另外一个函数的返回结果
作用:
1)把代码运算逻辑抽象出来,只要输出的结果
2)抽象出来的逻辑可以复用
2.1.1 函数作为参数
// forEach实现
function forEach (arr, fn) {
for (let i = 0;i < arr.length;i++) {
fn(arr[i], i)
}
}
// filter的实现
function filter (arr, fn) {
let tmpArr = []
for (let i = 0;i < arr.length;i++) {
if (fn(arr[i], i)) tmpArr.push(arr[i])
}
return tmpArr
}
let arr = [1, 5, 3, 4]
console.log('forEach------begin------')
forEach(arr, (item) => { console.log(item) })
console.log('forEach------end------')
console.log('filter------begin------')
console.log(filter(arr, (item) => item === 5))
console.log('filter------end------')
2.1.1 函数作为返回结果
比如一种支付场景希望只执行一次
function once (fn){
let done = false
return function() {
if(!done){
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once((amount) => {console.log(`支付的金额${amount}`)})
pay(5) // 只输出一次 支付的金额5
pay(6)
pay(6)
这里只会打印一次
模拟map、every、some
/**
* @description map函数
* @param {Array} arr 数组
* @param {Function} fn 需要执行的函数
*/
const map = (arr, fn) => {
let tmpArr = []
for(let i = 0; i < arr.length; i++){
tmpArr.push(fn(arr[i], i))
}
return tmpArr
}
// let arr = [1, 5, 3, 4]
// console.log(map(arr,(item) => ({name: item})))
/**
* @description every函数
* @param {Array} arr 数组
* @param {Function} fn 需要执行的函数
*/
const every = (arr, fn) => {
for(let i = 0; i < arr.length; i++ ){
if(!fn(arr[i], i)){
return false
}
}
return true
}
// let arr = [3, 3, 3, 3]
// console.log(every(arr, item => item === 3))
/**
* @description some函数
* @param {Array} arr 数组
* @param {Function} fn 需要执行的函数
*/
const some = (arr, fn) => {
for(let i = 0; i < arr.length; i++ ){
if(fn(arr[i], i))return true
}
return false
}
let arr = [2, 2, 4, 5]
console.log(some(arr, item => item === 3))
2.2 闭包
2.2.1 定义
闭包简单的来讲在另外一个作用域中可以调用函数内部函数并访问该函数作用域中的成员
const getSalary = function(base) {
return function(performance){
return base + performance
}
}
let base2500 = getSalary(2500)
let base5000 = getSalary(5000)
console.log(base2500(1000))
console.log(base2500(2000))
console.log(base5000(2000))
2.2 纯函数
2.1 定义
纯函数就是相同的输入永远得到的是相同的输出。而且没有任何观察的副作用。
简单来说,就是和数学中 x -> y 映射关系一样。
举例说明
Array.slice方法是纯函数
Array.splice 方法不是纯函数
- 函数式编程不会保留计算中间的结果。所以变量是不可变的(无状态)
- 可以利用lodash(纯函数)库,把一个纯函数的处理结果交给另外一个函数处理
2.2 好处
**1)**纯函数有相同的输入总会得到相同的输出,所以纯函数可以把得到的输出缓存起来
模拟lodash中memoize函数
// 模拟memoize函数
function memoize(f) {
let cache = {}
return function() {
let key = JSON.stringify(arguments)
return cache[key] = cache[key] || f.apply(f, arguments)
}
}
let getAreaWithMemory = memoize(getAraa)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 输出结果
4
50.26548245743669
50.26548245743669
50.26548245743669
50.26548245743669
2) 可测试性,纯函数方便测试
3) 并行处理
- 在多线程下操作函数中的共享内存数据可能会出现意外
- 纯函数不共享内存中的数据,所以可以进行多线程并行处理
2.3 副作用
举个例子
// 不纯
let age = 18
function compareAge() {
return age >= 18
}
// 纯函数
function compareAge() {
let age = 18
return age >= 18
}
副作用会让函数变得不纯,如上面的例子,纯函数是相同的输入会得到相同的输出,而不依赖外部状态。一旦外部状态改变,对输出的结果有影响就会产生副作用。
副作用的来源
- 全局变量
- 数据库
- 外部引用
- 配置文件
- …
所有外部交互都有可能带来副作用。副作用是函数的扩展性和可重用性下降。副作用不可避免,但是可以尽量减少副作用的产生
2.3 函数柯里化
定义:
- 当一个函数需要多个函数处理时,先传一部分参数给该函数处理(这部分参数以后不会改变)
- 然后返回一个函数处理剩余的参数,并处理结果
// 模拟实现函数柯里化
const getSum = function(a, b, c){
return a + b + c
}
function curry(fn) {
return function curryFn (...args){
// 比较实参和形参的个数
if(args.length < fn.length){
return function() {
return curryFn(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
let curried = curry(getSum)
console.log(curried(1, 2, 3))
console.log(curried(1)(2)(3))
// 返回结果
6
6
总结:
- 函数柯里化可以让我们给一个函数传递较少的参数得到一个记住了某些固定参数的形函数
- 这是一种对函数参数的缓存
- 让函数更加灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
2.4 函数组合
定义
函数组合:
如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并为一个函数
- 函数就像是数据的管道,函数组合将这些管道连接起来,让数据穿过多个管道
- 函数组合默认是从右到左执行
lodash中的flow 、flowRight就是组合函数
利用lodash实现
// 用函数组合的模式将NEVER SAY DIE 转换成 never-say-die
const _ = require('lodash')
const log = _.curry((tag, v) => {
console.log(tag, v)
return v
})
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 f = _.flowRight(join('-') , log('map之后'),map(_.toLower) , log('split之后'),split(' '))
console.log(f('NEVER SAY DIE'))
lodash中有fp模块(require(‘lodash/fp’))
fp模块特点
- 自动柯里化,函数优先,数据置后
改写后的代码如下
// 用函数组合的模式将NEVER SAY DIE 转换成 never-say-die
const _ = require('lodash')
const fp = require('lodash/fp')
const log = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const f = _.flowRight(fp.join('-') , log('map之后'),fp.map(_.toLower) , log('split之后'),fp.split(' '))
console.log(f('NEVER SAY DIE'))
模拟lodash中flowRight组合函数
// function compose(...args) {
// return function(value) {
// return args.reverse().reduce(function(acc, fn) {
// return fn(acc)
// }, value)
// }
// }
const commpose = (...args) => value => args.reverse().reduce(function(acc, fn){
return fn(acc)
}, value)
2.5 PointFree
PointFree:就是把数据的处理过程抽象成多个合成运算(即多个函数),只需要定义多个函数去处理就可以了,不需要关心数据的处理过程,函数组合就是用的PointFree
可以使用lodash的fp模块
2.5 函子(Functor)
定义:
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:函子就是一个特殊的容器,通过对象的方式实现,具有map方法,map方法可以运行一个函数对值的处理
// 函子
class Container {
constructor(value){
this._value = value
}
static of(value) {
return new Container(value)
}
map(fn) {
return Container.of(fn(this._value))
}
}
let con = Container.of(2).map(x => x + 8).map(x => x * x)
console.log(con)
总结:
- 函数式编程的运算不是直接操作值,而是通过函子完成的
- 函子就是一个实现了map函数的契约对象
- 我们可以把函子想象成一个盒子,这盒子里封装了值,
- 想要处理函子中的值,需要给map函数传递一个纯函数处理值。
- 最终map函数会返回一个包含新值的函子
2.5.1 Maybe函子
// 函子 maybe
// maybe函子可以处理一开始传null和undefined的情况,但是无法确定哪一步产生空值问题
class Maybe {
constructor (value){
this._value = value
}
// 生成新函子
static of(value) {
return new Maybe(value)
}
map(fn) {
return this.isNull() ? Maybe.of(null) : Maybe.of(fn(this._value))
}
isNull () {
return this._value === null || this._value === undefined
}
}
let functor = Maybe.of(4).map(x => x * x).map(() => null)
console.log(functor)
2.5.2 Either函子
// 函子 either
// 两者中的任何一个,类似于if else
// 异常会让函数变的不纯,Either函子可以用作异常处理
class Left {
constructor (value){
this._value = value
}
static of(value) {
return new Left(value)
}
map(fn){
return this
}
}
class Right {
constructor (value){
this._value = value
}
static of(value) {
return new Right(value)
}
map(fn){
return Right.of(fn(this._value))
}
}
function parseJson(str) {
try{
return Right.of(JSON.parse(str))
} catch(e) {
return Left.of({error: e.message})
}
}
let a = parseJson('{"name": "b"}').map(x => x.name.toUpperCase())
console.log(a)
let b = parseJson('{name: b}').map(x => x.name.toUpperCase())
console.log(b)
// 输出结果
Right { _value: 'B' }
Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
2.5.3 IO函子
// 函子 IO
// 函子IO中的_value是个函数,这里是把函数当做值处理
// IO函子可以把不存的动作存储到_value函数中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
// 把不纯的操作交给调用者处理
const fp = require('lodash')
class IO {
constructor(fn) {
this._value = fn
}
static of(x) {
return new IO(function(){
return x
})
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
let io = IO.of(process).map(p => p.execPath)
console.log(io)
console.log(io._value())
2.5.3 task函子
folktale 一个标准的函数式编程库
- 和 lodash、ramda 不同的是,他没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等
Task异步执行
const {split, find} = require('lodash/fp')
const { task } = require('folktale/concurrency/task')
const fs = require('fs')
const readFile = (filename) => {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json').map(split('\n')).map(find(x => x.includes('lodash'))).run().listen({
onRejected: err => { console.log(err) },
onResolved: data => { console.log(data) }
})
// 输出结果
"lodash": "^4.17.20"
2.5.3 Pointed函子
- Pointed 函子是实现了of静态方法的函子
- of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)
demo
class Container {
constructor(value){
this._value = value
}
static of(value) {
return new Container(value)
}
map(fn) {
return Container.of(fn(this._value))
}
}
2.5.3 Monad函子
- IO函子容易造成函子嵌套
- Monad函子可以扁平化函子,解决函子嵌套的问题,如果一个函数返回的是函子就用flatMap方法,如果返回的是值就用map方法
- Monad函数的特点有静态方法of和join方法,join方法返回的this._value()
// IO函子容易造成函子嵌套
// Monad函子可以扁平化函子,解决函子嵌套的问题,如果一个函数返回的是函子就用flatMap方法,如果返回的是值就用map方法
/// Monad函数的特点有静态方法of和join方法,join方法返回的this._value()
const { flowRight, toUpper } = require('lodash/fp')
const fs = require('fs')
class IO {
constructor(fn) {
this._value = fn
}
static of (value) {
return new IO(function () {
return value
})
}
map (fn) {
return new IO(flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
const readFile = (filename) => {
return new IO(function () {
return fs.readFileSync(filename)
})
}
const print = (x) => {
return new IO(function() {
console.log(x)
return x
})
}
const r = readFile('package.json')
.map(toUpper) // toUpper返回的是值所以用map
.flatMap(print).join() // print返回的是函子所以用flatMap
console.log(r)
// 输出结果
{
"DEPENDENCIES": {
"FOLKTALE": "^2.3.2",
"FS": "^0.0.1-SECURITY",
"LODASH": "^4.17.20"
}
}
{
"DEPENDENCIES": {
"FOLKTALE": "^2.3.2",
"FS": "^0.0.1-SECURITY",
"LODASH": "^4.17.20"
}
}