JS精要(2)-函数式编程
# 1.为什么要学函数式编程
- 函数式编程是随着React的流行受到越来越多的关注
- Vue3也开始拥抱函数式编程
- 打包过程中可以更好的利用tree shaking过滤无用代码
- 方便测试、方便并行处理
- 与很多可以帮助我们进行函数式开发:ladash、underscore、ramda
# 2.什么是函数式编程(FP)
- 思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 相同的输入始终得到相同的输出。
- 函数式编程用来描述数据(函数)之间的映射。
# 3.函数是一等公民
- 函数可以存储在变量
- 函数作为参数
- 函数作为返回值
- 把函数赋值给变量
# 4.高阶函数
- 函数作为参数或返回值
- 意义:抽象可以帮我们屏蔽细节,只需要关注我们的目标,高阶函数是用来抽象通用的问题
- 常用的高阶函数:数组的forEach、map、filter、every、some、find/findIndex、reduce、sort
# 5.闭包
- 闭包是指有权访问另一个函数作用域内变量的函数
- 本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
- 用途:
(1)在函数外部能够访问到函数内部的变量。
(2)使已经运行结束的函数上下文中的变量对象继续留在内存中。
function makePower(power){
return function(number){
return Math.pow(number,power);
}
}
let power2 = makePower(2);
let power3 = makePower(3);
1
2
3
4
5
6
7
2
3
4
5
6
7
# 6.纯函数
- 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- ladash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
- 数组的slice和splice分别是纯函数和不纯的函数
- 函数式编程不会保留计算中的结果,所以变量是不可变的
- 好处: (1)可缓存
const _=require('ladash');
function getArea(r){
console.log('圆面积');
return Math.PI * r * r;
}
let getAreaMemory = _.memoize(getArea);
console.log(getAreaMemory(4));
console.log(getAreaMemory(4));//圆面积只会打印一次
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
(2)可测试
(3)并行处理
# 7.副作用
- 副作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
- 副作用来源:配置文件、数据库、获取用户的输入。
- 所有的外部交互都有可能带来副作用,副作用使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。
# 8.柯里化
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余的参数,返回结果
- ladash中的柯里化案例:提取字符串和数字
//定义初始数据
let arr1 = ['Loki Handsome', 'Lucy_Candy']
let arr2 = ['123Lucy', '456Loki', '789Currency', 'Missing', "Loving"]
//定制柯里化函数
const match = _.curry((reg, str) => str.match(reg));
const haveSpace = match(/\s+/g);//正则匹配空格
const haveNumber = match(/\d+/g);//正则匹配数字
//创建过滤函数
const filter = _.curry((func, array) => array.filter(func));
//对函数进行进一步的封装
const findSpace = filter(haveSpace);
const findNumber = filter(haveNumber)
console.log(filter(haveSpace, arr1)); //[ 'Loki Handsome' ]
console.log(filter(haveNumber, arr2));//[ '123Lucy', '456Loki', '789Currency' ]
console.log(findSpace(arr1)); //简写写法
console.log(findNumber(arr2)); //简写写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 柯里化可以让我们给一个函数,传递较少的参数得到一个已经记住某些固定参数的新函数
- 这是一种对函数参数的缓存
- 让函数变得更灵活,让函数的耦合度更小
- 可以把多元函数换成一元函数,可以组合函数产生强大的功能
# 9.函数组合
- 纯函数和柯里化很容易写出洋葱代码 h(g(f(x))),函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
- 函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- ladash中的组合函数:flow()和flowRight();
- 结合律
- 函数组合调试
//ladash将NEVER SAY DIE转为never-say-die
const _ = require('lodash')
const map = _.curry((fn, array) => _.map(array, fn))
const split = _.curry((sep,str) => _.split(str,sep))
const join = _.curry((sep, array) => _.join(array, sep))
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 10.ladash中的FP模块
- lodash 的 fp 模块提供了实用的对函数式编程友好的方法
- 提供了不可变 auto-curried、iteratee-first、data-last
- ladash的参数为数据优先,函数滞后,FP模块的参数函数优先,数据滞后。
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
1
2
3
2
3
- lodash 和 lodash/fp 模块中 map 方法的区别
//_.map方法接受三个参数
const _ = require('lodash')
console.log(_.map(['23', '8', '10'], parseInt)) // => [23, NAN, 2]
//fp.map方法接受三个参数
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10'])) // => [23, 8, 10]
1
2
3
4
5
6
2
3
4
5
6
# 11.PointFree模式
- 是一种编程的风格,具体实现是函数组合,它更抽象一些,
- Point Free:我们可以把数据处理过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
(1)不需要指明处理的数据
(2)只需要合成运算过程
(3)需要定义一些辅助的基本运算函数(我们之前使用函数组合解决问题的时候其实就是 Point Free 模式)
// Hello World ---> hello_world
// 非Point Free
function f (world) {
return world.toLowerCase().replace(/\s+/g, '_')
}
//Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
// world wild web ==> W. W. W
const fp = require('lodash/fp')
// const f = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
const f = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(f('world wild web'))
1
2
3
4
5
2
3
4
5
# 12.函子Functor
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
let r = Container.of(5).map(x => x + 2).map(x => x * x)
console.log(r._value)
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数)有这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的盒子(函子)
# 12.1 MayBe函子
- 在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
// 正常使用时
// let r = MayBe.of('Hello World').map(x => x.toUpperCase())
// console.log(r) // MayBe { _value: 'HELLO WORLD' }
// 传入一个null或者undefined时
// let r = MayBe.of(null).map(x => x.toUpperCase())
// console.log(r) // MayBe { _value: null }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 12.2 Either函子
- Either 两者中的任何一个,类似于 if…else… 的处理
- 异常会让函数变得不纯,Either 函子可以用来做异常处理
const { truncate } = require("fs")
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
// let r1 = Right.of(12).map(x => x + 2)
// let r2 = Left.of(12).map(x => x + 2)
// console.log(r1) // Right { _value: 14 }
// console.log(r2) // Left { _value: 12 }
/**
* 我们需要先定义两个类,两个类之间有区别,主要是Map方法中有所区别
*
* 我们打印后的两个输出的结果是不一样的,两个代码都是一样的,只是一个使用left创建,一个试用right创建
*
* right中的值 +2 返回了14,但是 left 中直接是返回了传入的数据,没有做任何处理
*
* 我们可以对比两个类中的map方法
*
* Left 中 直接把当前对象返回了,并没有调用我们传入的fn,这样做是为了让Left中嵌入一个错误消息
*
* 下边我们来掩饰一个可能会发生错误的函数,比如我们要把一个json形式的字符串,转成一个json对象
* */
function parseJSON (str) {
try{
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({error : e.message})
}
}
// let r1 = parseJSON('{ name: zs }')
// console.log(r1) // Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
// let r2 = parseJSON('{ "name": "zs" }')
// console.log(r2) // Right { _value: { name: 'zs' } }
let r3 = parseJSON('{ "name": "zs" }').map(x => x.name.toUpperCase())
console.log(r3) // Right { _value: 'ZS' }
/**
* 我们先声明一个叫做 parseJSON 的函数,并给他接收一个参数
*
* 接下来我们要调用JSON.parse把我们传入的字符串转为json对象并且返回
*
* 因为调用JSON.parse时可能会出现异常,所以我们使用 try {} catch(e) {}
*
* 因为出现错误情况我们不去处理的话,那就不是一个纯函数
* 现在我们希望用函数式的方式去处理,所以我们需要写一个纯函数
*
* 我们现在要在try里return一个函子,我们会把我们转换后的结果交给这个函子,将来在这个函子内部去处理
*
* 如果出现错误我们是不能不管的,我们也要返回一个值,因为对于纯函数来说,对于相同的输入,始终要有相同的输出
*
* 那这个时候我们也要返回一个函子,因为我们Either中有Left和Right,我们用right去处理正确的值
* 如果出现异常的时候我们可以返回一个left中的函子,而left这个函子里边,可以帮我们去存储一些错误的信息
* */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 12.3 IO函子
- IO 函子中的 _value 是一个函数,因为函数是一等公民,所以这里是把函数作为值来处理
- IO 函子可以把不纯的动作存储到 _value中,_value中存储的是函数,我们在函子内部并没有调用这个函数,所以通过IO函数我们其实是延迟执行这个不纯的操作(惰性执行)包装当前的操作是一个纯的操作
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class Io {
static of (value) {
return new Io(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new Io(fp.flowRight(fn,this._value))
}
}
let r = Io.of(process).map( x => x.execPath)
console.log(r._value()) // C:\Program Files\nodejs\node.exe
/**
* constructor要去接收一个函数,因为IO函子里边保存的是函数,我们将接收到的函数保存在 _value 中
*
* of方法与之前有点差异,他要接受一个数据,然后返回一个新的IO函子
*
* 在这里便要返回一个IO函子,所以要调用IO构造函数,然后传入一个函数
* 因为刚刚我们写过它的构造函数,它的构造函数需要接收的是一个函数
*
* 所以在这里我们传入一个函数,在这个函数内,我们把刚刚of方法的值返回
*
* 到这里我们其实可以感受到IO函子,最终想要的还是一个结果,只不过他把取值的过程包装到了函数里边来
* 将来需要值的时候,在来执行这个函数,来取值
*
* map方法和之前一样还是需要传递一个fn,这个方法里边我们还是要返回一个IO函子,这里我们要调用IO的构造函数
* 而不是调用of方法,因为外部方法里面,我们把当前函子的value,也就是这个函数和我们传入的这个函数
* 组合成一个新的函数,而不是调用函数去给值 --- 这就是和以前不一样的地方
* */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 12.4 folktale
folktale 一个标准的函数式编程库
- 和lodash、ramda 不同的是,他没有提供很多功能函数
- 只提供了一些函数处理的操作,例如:compose、curry等,一些函子Task、Either、MayBe 等
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
// 这里的柯里化,与lodash中的柯里化,稍微有一点点的区别
let f = curry(2, (x, y) => x + y)
/**
* 两个参数:
* 第一个:指明我们后边这个函数他有几个参数,目的是为了避免一些错误
* */
console.log(f(1, 2))
console.log(f(1)(2))
console.log(f()(1, 2))
// compose --- 函数组合 --- 和lodash中的函数组合 flowRight 不一样
let f = compose(toUpper, first)
console.log(f(['one', 'two']))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 12.5 Task函子
//案例: 读取package.json,并将其中的version解析出来
const { task } = require('folktale/concurrency/task') // 导入task函子,路径可查官网
const fs = require('fs') // 导入fs模块
const { split, find } = require('lodash/fp') // 导入lodash/fp的方法
// 读取文件的函数,需要一个参数 --- 文件的路径,相对路径可以直接写文件名'package.json'
function readFile ( filename ) {
/**
* 返回一个task函子,需要接受一个函数,函数的参数是固定的resolver,可以通过官网来查询
* resolver是一个对象,里边提供了两个方法:resolve() 和 reject() --- 类似于promise
* */
return task(resolver => {
// 异步读取文件,需要三个参数:1---读取的路径;2---使用的编码;3---回调函数(在node中是错误优先);
fs.readFile(filename, 'utf-8', (err, data) => {
// 判断读取文件时是否出错
if(err){
// 失败时调用
resolver.reject(err)
}
// 成功时调用
resolver.resolve(data)
})
})
}
// 传入路径
readFile('package.json') //返回Task函子
.run() //读取文件
.listen({ //监听执行状态
onRejecter: err => { // 失败时执行的
console.log(err)
},
onResolved: value =>{ // 成功是执行的
console.log(value)
}
})
// 以换行为切割,分成数组,然后再去其中寻找version
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejecter: err => {
console.log(err)
},
onResolved: value =>{
console.log(value)
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 12.6 Pointed函子
- Pointed函子时实现了of静态方法的函子
- of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用Map来处理值)
class Content {
static of (value) {
return new Content(value)
}
//......
}
let c = Content.of(2).map(x => x + 3)
console.log(c)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 12.7 IO函子问题
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (value) {
return new IO(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO (function () {
console.log(x)
return x
})
}
let cat = fp.flowRight(print, readFile)
/**
* 结构:
* IO(IO(x))
*
* 外层IO是print返回的这个函子
* 内层IO是readFile返回的函子
*
* 因为先调用readFile这个函子,我们把readFile返回的这个函子传递给了print这个函数
*
* 所以print里边new IO内部输出的x其实就是readFile返回的这个函子,所以我们现在拿到的结果是一个函子嵌套了一个函子
* */
let r = cat('package.json')
// console.log(r) // IO { _value: [Function] }
// console.log(r._value()) // IO { _value: [Function] } IO { _value: [Function] }
console.log(r._value()._value()) // IO { _value: [Function] } { ... 打印出了package.json ...}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
- 现在我们的代码已经结束了,效果也完成了,但是也可以看出,我们在调用嵌套函子中的函数时,非常的不方便
- 如果函子有嵌套的话,我们想要调用嵌套函子中的函数的时候我们需要 r._value()._value() 这样确实是可以实现,但是看起来确实不太合适
- 所以我们想要把它改造一下,如何改造就需要用到 Monad(单子) 了
# 12.8 Monad(单子)函子
- Monad函子是可以变扁的Pointed函子,IO(IO(x)),解决函子嵌套问题。
- 一个函子如果具有 join 和 of 两个方法并遵循一些定律就是一个Monad
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (value) {
return new IO(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
/**
* 我们在使用monad的时候,经常会把,join和map联合起来使用
* 因为map的作用是把我们当前这个函数和我们函子内部的_value组合起来,返回一个新的函子
*
* map在组合这个函数的时候这个函数最终也会返回一个函子,所以我们需要调用Join将其变扁
*
* flatMap的作用就是同时调用map和join
* */
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new IO (function () {
console.log(x)
return x
})
}
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
/**
* 调用readFile的时候会返回一个函子,函子里边包裹了我们读取文件的操作
*
* 我们需要将读文件的操作和打印的操作合并起来
*
* 也就是接下来要将print调用进来
*
* 调用 map 还是 flatMap 取决于当我们要合并的这个函数
* 返回的直接是值就用map,返回的是一个函子我们就需要使用flatMap了
* */
console.log(r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- 什么时候使用monad:
当一个函数返回一个函子的时候我们要想到monad,monad可以帮我们解决函子嵌套的问题
当我们想要合并一个函数,并且这个函数直接返回一个值,这个时候我们可以调用map方法
当我们想要合并一个函数,这个函数想要返回一个函子,这个时候我们要用flatMap方法
上次更新: 2020/10/30, 18:10:00