loadsh中的一些纯函数及JavaScript中的函子
一.loadsh中常见的纯函数
主要有 first / last / toUpper / reverse / each / includes / find / findIndex等
先引入loadsh
const _ = require("lodash");
定义一个数组
const array = ["jack", "tom", "jerry", "kate"];
1. lodash.first()
获取数组中的第一个参数
console.log(_.first(array)); //jack
2. lodash.last()
获取数组中的最后一个参数
console.log(_.last(array)); //kate
3. lodash.toUpper()
将字符串转为大写
console.log(_.toUpper(_.first(array))); //JACK
4. lodash.reverse()
颠倒数组顺序
console.log(_.reverse(array)); //[ 'kate', 'jerry', 'tom', 'jack' ]
5.lodash.each()
循环数组
const r = _.each(array, (item, index) => {
console.log(item, index);
//jack 0
//tom 1
//jerry 2
//kate 3
});
console.log(r); //["jack", "tom", "jerry", "kate"]
6. lodash.includes()
判断数组中是否包含某个元素,返回true 或 false
console.log(_.includes(array, "tom")); //true
console.log(_.includes(array, "rose")); //false
7. lodash.find()
根据下标查找数组中的某个元素
console.log(_.find(array, 1)); //jack
8.Loadsh.findIndex()
用于查找元素首次出现的索引。它与indexOf不同,因为它采用了遍历数组每个元素的谓词函数。当元素存在时,返回元素的下标,元素不存在时,返回-1
let index = _.findIndex(
array,
(item) => {
return (item = "tom");
},
0
);
console.log(index); //0
let index = _.findIndex(
array,
(item) => {
return (item = "rose");
},
0
);
console.log(index); // -1
当元素在某个索引“i”之后被查找时。这里的元素存在于数组中,但输出仍然为-1,因为它存在于索引0中,并且搜索从索引1开始。
let index = _.findIndex(
array,
(item) => {
return (item = "tom");
},
1
);
console.log(index); // -1
findIndex() 接收三个参数,第一个参数为要查找的数组,第二个参数为数组遍历后的需要满足的条件,第三个参数为从哪个索引开始查找.
注:第三个参数可以省略,省略时默认从0开始查找
二.Functor(函子)
什么是Functor
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数,对值进行处理(变形关系)
案例:
//定义一个容器
class Container {
constructor(value) {
//私有属性
this._value = value
}
map(fn) {
return new Container(fn(this._value))
}
}
//返回新的函子对象
let r = new Container(5).map((x) => x + 1).map((x) => x * x)
console.log(r) //Container { _value: 36 }
此时,每次调用Container,都要通过new方法来进行调用
我们可以通过在class中定义static方法来进行改造:
class Container {
constructor(value) {
//私有属性
this._value = value
}
map(fn) {
return Container.of(fn(this._value))
}
//定义静态方法
static of(value) {
return new Container(value)
}
}
如此一来,我们就可以通过函数调用来调用Container类
let r = Container.of(5)
.map((x) => x + 2)
.map((x) => x * x)
console.log(r) //Container { _value: 49 }
总结:
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们就需要给盒子的map方法传递一个处理之的函数,由这个函数对值进行
处理 - 最终map方法返回一个包含新值的盒子(函子)
JavaScript中常用的函子
1.MayBe函子:
-
我们在变成的过程中可能会遇到很多错误,需要对这个错误做相应的处理
-
MayBe函子的作用就是对外部的空值情况做处理(控制副作用在允许的范围内)
案例 :
//定义一个MayBe函子
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
map(fn) {
//参数为null时,返回value 为 null
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
//判断参数是否为null
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时不会报错
let r2 = MayBe.of(null).map(x => x.toUpperCase())
console.log(r2) //MayBe { _value: null }
如果map中间有好几步,最后返回null,但是并不知道那一步出现了问题
let r3 = MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.splice(''))
console.log(r3) //MayBe { _value: null }
为解决这个问题, 需要看下一个函数
2.Either函子
- Either 两者中的任何一个,类似于 if..else..的处理
- 当出现问题的时候,Either函数会给出提示的有效信息,
- 异常会让函数变的不纯,Either函子可以用来做异常处理
因为是二选一,所以要定义两个函子
//错误函子
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 = left.of(12).map(x => x + 2)
let r2 = Right.of(12).map(x => x + 2)
console.log(r1) //left { _value: 12 }
console.log(r2) //left { _value: 14 }
为什么返回的结果会不一样呢,因为left返回的是this(当前对象),并没有调用fn
那么如何处理异常呢 ?
我们定义一个字符串转换成对象的函数
const parseJson = (str) => {
//对于可能出现错误的环节使用 try catch
//正确的情况使用Right函子
try {
return Right.of(JSON.parse(str))
} catch (e) {
//错误的情况使用left函子,并返回错误信息
return left.of({ err: e.message })
}
}
let error = parseJson('{name:zs}')
console.log(error) //left { _value: { err: 'Unexpected token n in JSON at position 1' } }
let success = parseJson('{"name":"zs"}')
console.log(success) //Right { _value: { name: 'zs' } }
console.log(success.map(x => x.name.toUpperCase())) //Right { _value: 'ZS' }
3. IO函子
- IO就是输入输出,IO函子中 _value 是一个函数,这里是把函数作为值来处理
- IO 函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作,把不纯的操作交给调用者处理(甩锅)
const fp = require('lodash/fp')
class IO {
//of方法快速创建一个值返回一个函数,将来需要的时候再调用函数
static of(value) {
return new IO(() => value)
}
//传入的是一个函数
constructor(fn) {
this._value = fn
}
map(fn) {
//这里用的是new一个新的构造函数,是为了把当前_value的函数和map传入的fn进行组合成新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
node执行环境可以传入一个process对象(进程)
调用of的时候把当前取值的过程包装到函数里面,再在需要的时候再获取process
const R = IO.of(process)
//map需要传入一个函数,函数需要接受一个参数,这个参数就是of中传递的参数process
//返回一下process中的execPath属性,即当前执行路径
.map(p => p.execPath)
console.log(R) //IO { _value: [Function (anonymous)] }
//上面返回的是一个函数,如需获取路径可执行下面的代码
console.log(R._value()) //C:\Program Files\New Folder\node.exe
4.task函子
const { task } = require('folktale/concurrency/task')
const fs = require('fs')
const { split, find } = require('lodash/fp')
//读取文件
function readFile(fileName) {
//task传递一个函数,参数是resolver
return task(resolver => {
//node中读取文件,第一个参数是文件路径,第二个参数是编码方式,第三个参数是回调(错误在前面)
fs.readFile(fileName, 'utf-8', (err, data) => {
if (err) {
resolver.reject(err)
} else {
resolver.resolve(data)
}
})
})
}
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
}) //"version": "1.0.0",
5.monad函子
- 用来解决io函子多层嵌套问题
IO函子中的问题:
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
//of方法快速创建一个值返回一个函数,将来需要的时候再调用函数
static of(value) {
return new IO(() => value)
}
//传入的是一个函数
constructor(fn) {
this._value = fn
}
map(fn) {
//这里用的是new一个新的构造函数,是为了把当前_value的函数和map传入的fn进行组合成新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
//读取文件
let readFile = (fileName) => {
return new IO(() => {
return fs.readFileSync(fileName, 'utf-8')
})
}
//打印函数
let print = (value) => {
return new IO(() => {
console.log(value)
return value
})
}
//组合函数,先读文件再打印
let cat = fp.flowRight(print, readFile)
let r = cat('package.json')
//拿到的是嵌套的IO函子 IO(IO())
console.log(r) //IO { _value: [Function (anonymous)] }
console.log(r._value()._value())
/**
* {
"name": "pd_funxtor",
"version": "1.0.0",
"description": "",
"main": "either.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"folktale": "^2.3.2",
"lodash": "^4.17.21"
}
}
*/
上面遇到多个IO函子嵌套的时候,_value就会调用很多次,这样的调用体验很不好,所以进行优化
什么是Monad函子
- Monad函子是可以变扁的Pointed函子,用来解决IO函子嵌套问题
- 一个函子如果具有join 和 of 两个方法并遵守一定的规则,就是Monad函子
实现一个Monad函子
实际开发中 不会这么难,主要是知道Monad的实现
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
//of方法快速创建一个值返回一个函数,将来需要的时候再调用函数
static of(value) {
return new IO(() => value)
}
//传入的是一个函数
constructor(fn) {
this._value = fn
}
map(fn) {
//这里用的是new一个新的构造函数,是为了把当前_value的函数和map传入的fn进行组合成新的函数
return new IO(fp.flowRight(fn, this._value))
}
join() {
return this._value()
}
flatMap(fn) {
return this.map(fn).join()
}
}
//读取文件
let readFile = (fileName) => {
return new IO(() => {
return fs.readFileSync(fileName, 'utf-8')
})
}
//打印函数
let print = (value) => {
return new IO(() => {
console.log(value)
return value
})
}
let r = readFile('package.json')
.flatMap(print)
.join()
console.log(r)
//执行顺序
/**
* readFile读取了文件,然后返回了一个 IO函子
* 调用flatMap使用readFile返回的IO函子调用的
* 并且传入了一个print函数
* 调用flatMap的时候 内部先调用map,当前的print和this._value进行合并,返回一个新的函子
* flatMap中的map函数执行完,print函数返回的一个IO函子,里面包裹的还是一个IO函子
* 下面调用join函数,返回内部的this._value
* 这个this._value就是之前print和this._value的组合函数,调用之后返回的就是print的返回结果
* 所以flatMap执行完毕之后,返回的就是print函数返回的IO函子
*/