目录
1.概念
FP,Functional Programing.
常见的编程范式:
- 面向过程编程:按照步骤实现想要的功能;
- 面向对象编程:把现实世界中的事物抽象成程序世界中的类和对象;
- 函数式编程:把现实世界中的事物和事物之间的联系抽象到程序世界,即对运算过程进行抽象;
函数式编程中的函数,不是指程序中的函数(方法),而是指数学中的函数(映射)。
Demo: 计算两个数的和
// 面向过程编程
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
// 函数式编程
function add (a1, a2){
return a1 + a2
}
let sum = add(2, 3)
console.log(sum)
2.知识回顾:
2.1 函数是一等公民
First-class Function, 头等函数
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
2.2 高阶函数
Higher-order Function
- 函数可以作为参数传递给另一个函数,
如数组的 forEach()、filter()、sort() 方法 - 也可以作为另一个函数的返回值
Demo: 让某个函数只执行一次
function once(fn){
let done = false
return fuction(){
if(!done){
fn.apply(this, arguments)
}
}
}
let pay = once(function(money){
console.log(`¥${money}`)
})
console.log(pay)
/*结果:
function(){
if(!done){
done = true
fn.apply(this, arguments)
}
}
*/
pay(5);
pay(3);
pay(2);
// 结果:¥5
// once函数只执行了一次,pay函数,即执行once时返回的那个function执行了多次
2.3 闭包
Closure:“定义在一个函数内部的函数”。如⬆️的pay()
作用:可以在另一个作用域中调用一个函数(once)的内部函数(pay),并访问到该函数作用域中的成员(done)。
3.纯函数
函数式编程中的函数,指的是 纯函数,类似数学中的函数( y=f(x) )。
相同的输入(参数)永远会得到相同的输出(返回值),而且没有任何可观察的副作用。
- 可缓存
- 可测试
- 并行处理(多线程,ES6 Web Worker)
3.1 副作用
如果函数依赖于外部状态,就无法保证相同输入每次的输出都相同,就会带来副作用。
副作用的来源:
- 函数外部的变量
- 配置文件
- 数据库
- 获取用户的输入
…
副作用不可能完全禁止,但要尽量控制在可控范围内。
3.2 Lodash
一个原生的js库,降低了对数组、对象等的使用难度。
// 模拟 lodash 的记忆函数 _.memoize()
// _.memoize()的参数是一个纯函数,返回值是一个带有记忆功能的函数
function memoize(fn){
let cache = {}
return function(){
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
4.柯里化
当一个函数有多个参数的时候,先传递一部分参数,然后返回一个函数接收剩余的参数。
// 普通的纯函数
function checkAge (min, age){
return age >= min
}
checkAge(18, 19)
checkAge(20, 22)
// 柯里化
function checkAge(min){
return function(age){
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(19)
checkAge20(22)
4.1 lodash中的柯里化函数
curry(func),他的作用是,创建一个函数,该函数可以接受一个或多个func的参数,如果func所需的参数都被提供,则直接执行func并返回结果,否则继续返回该函数并等待接收剩余的参数
const _ = require('lodash')
// 要柯里化的函数
function getSum(a, b, c){
return a+b+c
}
// 柯里化后的函数
let curried = _.curry(getSum)
console.log(curried(1,2,3)) //6
console.log(curried(1)) //getSum(a,b,c)
console.log(curried(1)(2,3)) //6
console.log(curried(1,2)) //getSum(a,b,c)
// 模拟curry函数, 参数是一个纯函数(func),返回值也是一个纯函数
function curry (func){
return function curriedFn(...args){
//如果参数少于func所需的所有参数,返回该函数,继续接收剩余的参数
if(args.length < func.length){
return function (){
return curriedFn(...args.contact(Array.from(arguments))
}
}
//如果func所需的参数都被提供,则直接执行func并返回结果
return func(...args)
}
}
5.函数组合 Compose
如果一个输入要经过多个函数处理才能得到输出,可以把中间过程的函数合并成一个函数。
- 函数组合默认从右到左执行
- 函数组合要满足结合律,可以先结合f1和f2, 也可以先结合f2和f3, 结果是一样的
5.1 lodash中的组合函数
- flow(),从左到右执行
- flowRight(),从右到左执行
5.2 模拟flowRight函数
// 参数是多个函数,返回值是组合后的函数
function compose (...args){
return function(value){
return args.reverse().reduce(function(acc, currentFn){
return currentFn(acc)
}, value)
}
}
//ES6写法
const compose = (...args) => (value) => args.reverse().reduce((acc, currentFn) => currentFn(acc), value)
5.3 调试组合函数
可以定义一个跟踪函数,来查看组合函数每一步执行的结果。
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const f = _.flowRight(fn1, trace(fn2), fn2, trace('fn3'), fn3)
5.4 lodash-fp模块
- 自动柯里化
- 函数优先,数据滞后(与lodash模块中的方法相反)
5.5 PointFree
一种编码风格,实际上还是函数的组合
- 不需要指明处理的数据
- 只需要合成中间的运算过程
- 需要定义一些辅助的基本运算函数
6.函子 Functor
一个包含值(value)和值的变形关系(map函数)的容器,通过一个对象来实现,该对象具有map方法,map方法运行一个函数(fn)来对值进行处理。
class Container {
static of(value){ //静态方法
return new Container(value)
}
constructor(value){
this._value = value //私有属性
}
map(fn){
// return new Container(fn(this._value))
return Container.of(fn(this._value))
}
}
let container = new Container(2)
let r1 = new Container(2)
.map(x => x+1)
.map(x => x*x)
let r2 = Container.of(3)
.map(x => x+2)
.map(x => x*x)
console.log(typeof(Container)) // function,构造函数
console.log(typeof(container)) // obj: Container{_value: 2}
console.log(r) //obj: Container{_value: 4}
// 每次调用map都返回一个新的函子(包含新的value),因此可以继续调用它的map函数
6.1 Maybe函子
当外部传入空值时做出处理,但不能确定是哪一步产生的空值
6.2 Either函子
类似 if…else…的处理,需要定义两个类,分别处理正常情况和异常情况。
6.3 IO函子
IO函子传入的值是一个函数
class IO {
static of(x) {
return new IO(function () {
return x
})
}
constructor(fn){
this._value = fn
}
map(fn) {
// 把当前的 _value 和 传入的 fn 组合成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
Folktale,一个标准的函数式编程库,提供了一些函数式处理的操作,如compose,curry,和一些函子Task, Either, Maybe等
6.4 Task函子
- task函数用来处理异步操作,
- 接收一个函数,这个函数的参数是一个对象 resolver,resolver提供两个方法:resolve 和 reject;
- 返回值是一个Task函子。
function taskTest(){
return task(resolver => {
if(false) resolver.reject()
resolver.resolve()
})
}
// 执行taskTest函数,返回一个Task函子
taskTest()
.map() //在这里对数据进行处理
.run() //调用run执行
.listen({ //在这里传入 resolve 和 reject
onReject: err => {
},
onResolve: data => {
}
})
6.5 Pointed函子
实现了of静态方法的函子。
- of方法可以避免使用new来创建对象
- of方法用来把值放到上下文Context,使用map方法来处理
6.6 Monad函子(单子)
可以变扁的Pointed函子 解决函子嵌套问题 IO(IO(x))
class IO(){
static of(x){
return new IO(function(){
return x
})
}
constructor(fn){
this._value = fn
}
map(fn){
return new IO(fp.flowRight(fn, this._value))
}
join(){
return this._value
}
flatMap(fn){
return this.map(fn).join()
}
}
- map 方法,对值(this._value)进行处理
- join 方法,用来把嵌套的函子“拍扁”,把函子的值取出来
- flatMap(fn),当要合并的的fn返回一个值时调用map, 当要合并的fn返回一个函子时调用flatMap