JavaScript-函数式编程范式

237 阅读10分钟

函数式编程

函数式编程是一种编程范式,一种编程思想

  • 函数式编程思维是将现实中一个东西和另一个东西之间的映射关系进行抽象,对运算过程进行抽象

  • 函数式编程中的函数是指数学中的函数 例如 y=sin(x) y=tan(x) 

  • 例如一个映射关系 y=f(n) 函数式编程是用于描述将y和x之间映射关系f()

  • 纯函数是指相同的输入始终得到相同的输出

    // 面向过程
    let a = 10
    let b = 5
    let sum1 = a + b
    
    // 函数式编程
    // add是一个映射关系的函数
    // 将运算过程进行了抽象
    function add (x, y) {
        return x + y
    }
    let sum2 = add(a, b)
    

函数式是一等公民

JavaScript中函数是一个对象,和普通的对象一样

  • 函数可以存储在变量中

  • 函数可以作为参数传给另一个函数

  • 函数可以返回值

    let fun1 = function () {
        console.log('fun1')
    } // 存储在变量中
    fun1()
    let fun2 = function (fun) { // 作为参数
        fun()
    }
    fun2(fun1)
    let fun3 = function () {
        return fun1
    }
    fun3()()
    

高阶函数

说明:将函数作为参数

高阶函数可以对程序中的一些处理进行抽象,屏蔽一些数据操作的细节,只需要关注目标数据

高阶函数可以对通用的数据处理进行抽象,方便在其他地方进行复用

例如JavaScript中数组的方法,就是讲通用的数组操作抽象起来,屏蔽操作的细节

// 模拟数组的forEach方法
function forEach (array, fn) {
    for (let index = 0; index < array.length; index ++){
        fn(array[index])
    }
}
forEach([1,2,3], (value) => { // 将fn传入处理
    console.log(value)
})

闭包

说明:函数和其周围的状态、作用域(词法环境)的引用捆绑在一起形成闭包

本质:函数在执行的时候会放到一个执行栈上,当函数执行完后会从执行栈上移除,但堆上的作用域成员因为被外部引用不能释放,所以内部函数可以访问外部函数的成员

表现:可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

function makeFn () {
  let msg = 'msg'  return function () { // 内部返回一个函数
    console.log(msg) // 访问到另一个作用域  }
}let fn = makeFn()
fn() // 打印msg// fn在另一个作用域调用makeFn内容函数function 并且访问到makeFn的msg```

纯函数

说明:相同的输入永远会得到相同的输出,多次重复和不同的环境下都是相同输入、相同输出,没有任何可观察的副作用

纯函数类似数学中的函数用来描述输入和输出之间的关系 y = f(x)

有纯函数和不纯的函数之分 JavaScript中的数组方法有例子

slice、forEach 操作不会改变原数组,所以多次传入相同原数组,结果是一样

splice、reserve 会改变原数组,多次执行会得到不同的输出

函数式编程不会保留计算中间的结果,变量是不可变的(相同输入、相同结果)无状态的

一个函数的执行结果可以交给另一个函数处理

函数的副作用

说明:函数有依赖外部的状态,一旦有这种依赖就无法保证相同输入得到相同输出,就会产生副作用

副作用的来源有多种,如读取配置文件、请求接口、获取用户输入等等,因为这些来源不确定,所以会导致函数不纯

副作用会给程序带来安全隐患(报错等等)和程序结果不确定性

副作用是不可能消除的,只能做到尽可能将副作用控制在可控制范围

纯函数中将外部依赖写在函数中,避免了影响,但也造成硬编码的问题

let mini = 18
// 不纯的函数
function checkAge (age) {
    // 函数内部依赖了外部的值 外部的值变化时会影响函数的返回 不能做到相同输入 相同输出
    return age >= mini
}
// 纯函数
function checkAge (age) {
    let mini = 18
    // 没有依赖外部的值 相同输入 得到相同输出 但在内部编码时固定了值
    // 可以用柯里化解决
    return  age >= mini
}

函数柯里化

说明:当一个函数需要多个参数时,先传入部分参数,调用它(传入的参数以后不再改变,作为外部依赖),返回一个新的函数接受剩余的参数,后面使用这个返回的函数

// 函数柯里化 先传入外部依赖的值 mini不再改变的
// 返回一个新的函数接受剩余的参数age
// 利用闭包可以访问到依赖mini
function checkAge (mini) {
    return function (age) {
        return age >= mini
    ]
}
// 返回一个新的函数 因为没有依赖外部的值 checkAge18 就是纯函数
let checkAge18 = checkAge(18)
checkAge(20)

将外部依赖先传入一个创建函数中,用闭包的方式,返回一个函数可以访问到这些外部依赖

这个返回的函数因为没有了外部依赖,就是一个纯函数了

这种方式是一种依赖就要一个创建函数,多种依赖搭配就要有多个创建函数

通过传入部分的参数得到记住参数的函数,对函数参数的一种存储

可以将多元函数变成一元函数,使用组合函数产生强大的函数

让函数更灵活,粒度更小

管道

程序中的函数可以想象成管道,从入口传入值a 经过管道fn后 得到值b

当将一个管道fn拆分成多个小的fn 值a按顺序进行多个fn得到值b

函数组合

说明:当需要经过多个函数处理后才能得到最终的值时,可以将中间过程的这些函数组成合并一个

纯函数和柯里化很容易将产生洋葱代码,函数写成一层套一层 b = f1(f2(f3(a)))

函数组合可以将这些细颗粒的函数组成一个新的函数

函数组合默认是从右到左执行

fn = compose(f1, f2, f3)

b = fn(a)

function compose(...args) {
    return function (value) {
        // 将传入的数组翻转后用reduce方法依次执行数组的元素fn 将传入的value作为第一次的值
        // 每次执行的值作为下一个fn调用的参数
        // 实现从右到左 组合依次调用
        return args.reverse().reduce(function (acc, fn) {
            return fn(acc)
        }, value)
    }
}
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
fn = compose(toUpper, first, reverse)

b = fn(['one', 'two', 'three'])

Point Free编程风格

说明:将数据处理处理的过程定义为和具体数据无关的合成运算,不需要在合成运算中用到具体的数据的参数,为此需要定义一下辅助函数

要点:

  • 不需要指明处理的数据
  • 只需要组合运算
  • 需要定义一些辅助的基本运算函数

函子Functor

目的:

  • 函数的副作用控制在可控范围
  • 异常处理
  • 异步处理

容器:包含值和值之间的变形关系(这个变形关系就是函数)

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

class Container {
    constructor (value) {
        // 保存值 不应该被外部直接访问
        this._value = value
    }
    // 用于创建函子对象
    static of (value) {
        return new Container(value)
    }
    // 接受处理的fn 只提供数据处理的入口和返回一个新的函子
    map (fn) {
        // fn处理_value 并且用这个处理后的值 创建返回一个新的函数
        // 返回一个新的函子对象可以继续链式调用
        return Container.of(fn(this._value))
    }
}
Container.of(5)
    .map(x => x + 1)
    .map(x => x * x)
// 函子里面的值不要被直接引用 值永远包含在函子里面

MayBe函子

说明:对外部传入空值进行处理(控制副作用)

class MayBe {
    constructor (value) {
        this._value = value
    }
    static of (value) {
        return new MayBe(value)
    }
    map (fn) {
        // 调用isNothing判断是否为空 空就返回一个新的函子 传入null 否则调用fn
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this_value))
    }
    // 判断_value 是否为空值
    isNothing() {
        return this._value === null || this._value === undefined
    }
}
MayBe.of(null)
    .map(x => x.toUpperCase())
// 在调用map时检查value是否为空值 是就不会执行fn 直接返回一个 新的函子 值为null
// 避免了调用map时值为空值 报错的问题
// MayBe函子可以控制空值报错的问题 但是当多个map调用时 无法知道哪一步map出现空值
MayBe.of(5).map(x => x + 1).map(x => null).map(x => x + 10)

Either函子

说明:Either 表示两者中任意一个,类似 if else,Either用来作异常处理

Left函子作为出现异常后返回有错误信息的函子

Right函子作为之前执行后返回的函子

class Left {
    constructor (value) {
        this._value = value
    }
    static of (value) {
        return new Left(value)
    }
    // Left的map是返回当前的函子 不会执行传入的fn
    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 r1 = parseJSON('abc') // 会出现异常 返回一个Left函子
r1.map(x => console.log(x)) // map就当前Left对象的实例

let r2 = parseJSON('{ "name": "abc" }') // 没有异常 返回一个Right函子
r2.map(x => console.log(x))

// parseJSON 相同输入 都得到返回 一个函子 是纯函数

IO函子

说明:将不纯的操作存储在函子的_value中,延迟不纯的操作给调用者,返回封装成纯函数

const fp = require('lodash/fp')
class IO {
    constructor (fn) {
        this._value = fn
    }
    static of (value) {
        // 把取值的过程包装到函数里面 需要的时候再取值
        return new IO(function() {
            return value
        })
    }
    map (fn) {
        // 返回一个新的IO函子 并且要将传入的fn和_value的函数组合起来传个新的函子
        return new IO(fp.flowRight(fn, this._value))
    }
}
let r = IO.of(5).map(x => x + 10)
// 传入of 值5 会将传入的value封装成function保存到当前IO实例的_value
// 调用map将传入的 x => x + 10 和_value 组合 返回保存在新的IO实例的_value
console.log(r._value()) // _value是一个函数

IO函子嵌套的问题

用函数组合返回IO函子的函数后,得到的是一个IO函子的嵌套,调用_value时因为多次嵌套很麻烦

const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
let fn1 = function (value) {
    return new IO(function () {
        return value * value
    })
}
let fn2 = function (x) {
    return new IO(function () {
        return x
    })
}
let fn3 = function (x) {
    return new IO(function () {
        return x
    })
}
let cat = compose(fn3, fn2, fn1)
let r = cat(10)._value()._value()._value()

Monad函子

说明:用于解决函子嵌套的问题

Monad函子需要具体join和of两个方法 join 返回_value()

const fp = require('lodash/fp')
class IO {
    constructor (fn) {
        this._value = fn
    }
    static of (value) {
        // 把取值的过程包装到函数里面 需要的时候再取值
        return new IO(function() {
            return value
        })
    }
    map (fn) {
        // 返回一个新的IO函子 并且要将传入的fn和_value的函数组合起来传个新的函子
        return new IO(fp.flowRight(fn, this._value))
    }
    join () {
        return this._value()
    }
    // 执行map和join
    flatMap (fn) {
        return this.map(fn).join()
    }
}
let fn1 = function (value) {
    return new IO(function () {
	console.log('fn1---value')
        return value * value
    })
}
let fn2 = function (x) {
    return new IO(function () {
	console.log('fn2---value')
        return x + 10
    })
}
let cat = fn1(10).flatMap(fn2).join()
console.log(cat)
// fn1 --- flatMap
// fn1 --- map
// fn1---value
// fn2---value
// 110