一、函数式编程概念
1、为什么要学习函数式编程
- 函数式编程随着react的流行受到越来越多的关注
- Vue3也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的利用tree shaking过滤无用代码
- 方便测试, 方便并行处理
- 有很多库可以帮助我们继续函数式开发:lodash、underscore、ramda
2、什么是函数式编程
函数式编程(FB),是编程范式之一, 我们常听说的编程还有面向过程编程、面向对象编程
思维方式: 把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 程序的本质: 根据输入通过某种运算获得相应的输出, 程序开发过程中会设计很多有输入和输出的函数
- x > f(联系、映射) > y, y = f(x)
- 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如: y = sin(x), x和y的关系
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射(其实就是对运算过程的重现)
非函数式(面向过程)
//先定义两个数
let num1 = 1;
let num2 = 2;
// 把这两个数相加, 把结果存储到一个变量中
let sum = num1 +num2;
// 最后通过.log打印出来
console.log(sum)
函数式(对运算过程进行抽象)
// 抽象一个add的函数, 这个函数会接收两个参数, 当这个函数执行完毕之后, 会把这个结果进行返回(函数要有相同的输入相同的输出)
function add(n1, n2) {
return n1 + n2
}
// 之后就可以调用这个函数, 得到相应的结果
let sun = add(2, 3)
//然后打印出来
console.log(sun)
二、函数是一等公民
在js中函数就是一个普通的对象, 可以通过new function()来进行创建, 因为是变量, 所以可以把函数存储在变量/数组中,或者作为函数的参数或者返回值,甚至在程序运行的时候通过new function()'alert(1)'的方式来构造一个新的函数
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
//把函数赋值给变量
//定义了一个函数, 把他赋值给fn这个变量,
let fn = function () {
console.log('Hello World')
}
// 再通过fn来调用这个函数
fn()
//定义了一个BlogController对象, 这个对象定义了几个方法
const BlogController = {
//index方法内用了另一个index方法,并且把他的结果返回
// index(posts)方法和我们内部调用的View.index(posts)有相同的形式,他的参数和返回值是一样的
// 如果以后遇到一个函数包裹了另一个函数, 并且他的形式也相同的时候,就可以认为这是两个一样的函数, 那我们就可以对代码进行精简
index (posts) { return View.index(posts) },
show (post) { return View.show(post) },
create (attrs) { return Db.create(attrs) }
}
//优化
// 将View.index赋值给index(是方法赋值给方法, 不是把调用赋值给方法)
const BlogController = {
// index: View.index(posts)
//我们赋值的是方法本身, 而不是这个方法的返回值
index: View.index
}
三、高阶函数
1、什么是高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
2、高阶函数-函数作为参数
// foreach: 遍历数组的每一个函数, 对函数做相应的处理
// 模拟 forEach时接收参数:数组(因为我们要遍历参数),遍历数组只有要对数组每一项做处理, 有可能打印,有可能输出, 所以再增加一个变化的量
function forEach (array, fn) {
//遍历数组中的每一个元素
for (let i = 0; i < array.length; i++) {
//处理数组中的每一个元素(因为处理的方式不同,所以用fn)
fn(array[i])
}
}
//测试
let arr = [1, 3, 4, 7, 8]//创建一个数组
// 传入一个数组,第二个参数就是我们之前forEach里的fn
// fn中有个参数,用来定义数组中的每一项,所以function中需要一个参数,来接收数组中的每一项"item"
forEach(arr, function (item) {
console.log(item)
})
//filter
//过滤数组中满足条件的函数,先把数组都存储起来,把满足条件的数值进行返回
//filter里面有两个参数,第一个是我们需要过滤的数组,第二个是满足什么样的条件
function filter (array, fn) {
//定义一个数组,用来存储得到的结果
let results = []
// 遍历这个数组
for (let i = 0; i < array.length; i++) {
//判断这个数组是否满足我们的条件
if (fn(array[i])) {
// 如果满足条件,需要把数据存放到results里面
results.push(array[i])
}
}
//循环之后,返回我们的结果
return results
}
/* //测试
let arr = [1, 3, 4, 7, 8]//创建一个数组
//传入我们定义的数组,传入我们定义的条件
// fn调用时候传递了一个参数,所以再定义的时候也要传递一个参数
// filter(arr, function(item)) {
//定义一个变量接收这个返回数组
let r = filter(arr, function(item) {
//在这个函数中,指定我们寻找的条件
//判断是否为偶数
return item % 2 === 0
//在调用函数中'fn(array[i])'中为true(item为偶数),则将数值存放到results(结果数组)中来
})
console.log(r) */
总结: 函数作为参数来调用更为灵活, 在调用forEach时候,不需要考虑内部是如何实现的(forEach、filter通过名字就知道是遍历和过滤)
3、函数作为返回值
// 让一个函数去生成一个函数
// function makeFn () {
// //定义一个变量
// let msg = 'Hello function'
// //需要返回一个函数,通过匿名函数来实现
// return function () {
// console.log(msg)
// }
// }
//测试
//调用方式一
// //第一次调用makeFn函数时候,它是返回了一个函数,需要去接收一下这个函数
// const fn = makeFn()
// // 这个函数需要再调用一次
// fn()
// //调用方式二
// // 第一个括号是调用makeFn()这个函数,第二个括号是调用里面这个返回的函数
// makeFn()()
// once
// jq中的once是给dom元素注册事件,这个事件只会被执行一次
// 在'lodash'中有一个once函数, 是对函数只执行一次
// 应用场景: 支付流程,只允许支付一次
function once (fn) {
// 定义一个变量, 这个变量就是一个标题, 用来记录我们的fn是否被执行了
let done = false; // 默认未被执行
//定义一个返回函数
return function () {
// 判断done的值是true还是false,done为true时,函数不会被执行
if(!done) {
// 如果done为false,则让done为true
done = true
// 把用户调用fn的参数带进去
// 调用的时候可以改变他的this,不过目的不是为了改变this,而且当前也不会用到
// 第二个参数是调用function()时候传递的参数,调用function()其实就是希望调用fn, 所以我们把function()传递给fn
// 想要知道是否被执行, return返回的结果是true还是false
return fn.apply(this, arguments)
}
}
}
// 通过once生成了一个函数,而这个函数的调用在fn的内部只有一次
// 测试
// 调用once函数,生成一个只能够执行一次的函数
// 需要传入金额
let pay = once( function (money) {
console.log(`支付: ${money} RMB`)
// 此处不是单引号(''),而是模板字符串(``)
// ` 是ES6中的新特性,拼接字符串的时候可以直接使用变量
})
// 多次调用
pay(5)
pay(5)
pay(5)
pay(5)
4、使用高阶函数的意义
- 函数是编程的核心思想, 需要对运算过程进行抽象, 然后可以再很多地方去重复用这个函数
- 抽象可以帮我们屏蔽细节, 只需要关注我们的目标
- 高阶函数是用来抽象通用的问题
循环打印一个数组中的数组元素
//面向过程的方式
// 定义一个数组
let array = [1, 2, 3, 4]
// 写一个for循环, 定义一个循环变量,并初始化循环变量,并判断循环变量是否小于循环变量的长度,然后再让循环变量++
for (let i = 0; i < array.length; i++) {
console.log(array[i])
}
// 这种方式需要关注循环的细节,比如循环变量的控制
// 高阶函数
// 帮我们把循环的过程抽象成一个函数forEach,当我们在循环的时候, 不需要知道forEach你内部的细节, 只需要知道forEach帮我们完成了循环就行了
let array = [1, 2, 3, 4]
forEach(array, item => {
console.log(item)
})
// 过滤数组中的函数,只要是过滤数组就可以使用它, 过滤的条件是传递的函数来调用的
let r = filter(array, item => {
return item % 2 === 0
})
forEach和filter都是对通用问题的一个抽象, 在使用的过程中不需要知道他们内部的细节
- 可以使我们的函数变得很灵活
- 可以使代码变得更简洁
- 抽象可以帮助我们屏蔽使用的细节
5、常用的高阶函数
⑴、forEach和filter在上段已经展示
⑵、map: 对每一个元素进行遍历, 并对每一个元素进行处理, 然后将处理的结果存储到一个新的数组中进行返回
// 目的:对每一个元素进行遍历, 并对每一个元素进行处理, 然后将处理的结果存储到一个新的数组中进行返回
// 对函数的定义是用函数表达式的方式,因为不希望别人去修改, 所以用的是const常量的方式
// 可以用匿名函数和箭头函数, 箭头函数更加简洁
// 参数: 因为map函数需要对数组进行遍历, 所以我们第一个传入的参数是数组; 当对数组遍历的过程中, 需要里面的每一个元素进行处理, 所以还需要一个函数
const map = (array, fn) => {
// 需要对每一个元素进行处理, 并把处理的结果存储到一个新的数组中来, 定义一个数组, 储存这个结果
let results = []
// 遍历数组, 去取出数组中的每一个元素
// for循序安和for of都可以, 其实for of是for循环的一中抽象
// 将数组中的每一项存储到value中来
for (let value of array) {
// 通过fn处理value中的每一个元素, 再把处理的元素存储到results中来
results.push(fn(value))
}
return results
}
// 测试
let arr = [1, 2, 3, 4]
// 因为map函数会返回一个新的函数,所以用arr来接收
// 传入两个参数: 第一个是数组, 第二个是如何处理数组中的每一个元素(这个函数在调用时候需要一个参数,所以在传入的时候也要用一个参数)
arr = map(arr, v => v * v)
console.log(arr)
总结: map的参数是一个高阶函数, 他的好处是不光可以对数组里面的每一个元素求平方,可以通过制定第二个参数的函数,对数组中的每一个元素做任意的修饰,所以函数作为参数,会让我们的map函数更灵活
⑶、every: 判断数组中的每一个元素是否都匹配我们指定的条件(条件是函数的形式)
// 定义const every等于一个箭头函数; 两个参数, 数组--我们要找到数组中的每一个元素,fn--通过fn来检测,是否满足检测的条件
const every = (array, fn) => {
// 因为最终是要判断和引入的条件是否匹配,所以先定义一个变量, 假设所有的元素都是匹配的
let result = true
// 将数组中的所有元素全部遍历一遍
for (let value of array) {
// 判断元素是否匹配我们的条件,并把结果记录到result中来
result = fn(value)
// 如果有元素不满足条件, 直接返回false
if(!result) {
break
}
}
return result
}
// 测试
// 初始化数组
// let arr = [11, 13, 14]
let arr = [9, 13, 14]
// 检测完之后,会返回一个布尔类型的值,我们来接受
// 要检测数组中的每一个元素是否都>10
let r = every(arr, v => v > 10)
console.log(r)
⑷、some: 和every非常类似,是用来检测数组中的元素是否有一个满足条件
const some = (array, fn) => {
// 定义一个变量,假设数组内的所有元素都不满足条件
let result = false
// 将数组遍历
for (let value of array) {
// 判断元素是否符合我们的条件, 并把结果记录到result中来
result = fn(value)
// 如果有一个元素符合条件,就停止循环
if (result) {
// 跳出该switch语句体
break
}
}
return result
}
// 测试
// 定义一个数组
// let arr = [1, 3, 4, 5]
let arr = [1, 3, 5, 9]
// 定义一个变量接收判断返回的结果
let r = some(arr, v => v % 2 === 0)
console.log(r)
四、闭包
1、闭包概念
- 闭包(closure): 函数和其周围的状态(词法引用)的引用捆绑在一起形成闭包
- 可以在另一个作用域中调用一个函数的内部函数,并访问到该函数的作用域中的成员
2、语法演示: 函数作为返回值(中的案例)
// 首先定义了makeFn()这个函数,在这个函数中返回了一个函数,当调用makeFn时候,这里面定义了一个变量msg,当这个函数执行完毕,函数内部的成员就会被释放掉;如果这函数里面有一个返回函数,这个返回函数又访问了函数外部的成员,这就是闭包
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
// 当我们调用makeFn()时,会返回fn这个函数,其实就引用了makeFn()这里面的函数,外部对内部的函数有引用;当我们外部对内部有引用的时候, 内部的这些成员是不能被释放
// 闭包: 在makeFn()这个相同作用域中去调用一个函数内部的函数(function())时,内部函数可以访问到makeFn作用域中的成员
const fn = makeFn()
fn()
- 正常情况: makeFn执行完成后, msg会被释放掉
- 闭包: 如果makeFn中返回了一个成员, 并且外部对这个成员有引用,此时这个成员对msg有引用,那么msg就不会被释放
- 在一个作用域中,可以调用这个函数的内部函数
- 当调用函数时,可以访问到函数作用域的内部成员
- 闭包的作用就是把内部函数的作用范围延长了
3、实际应用: 支付流程,只允许用户支付一次
// 作用:确保传入的fn只被执行一次(控制: 需要对它进行标记,如果标记被执行了,那以后都不会对他再次执行)
function once (fn) {
// 定义了一个局部变量,默认情况下fn是没有被执行的
let done = false
// 在once的内部返回了一个函数
return function () {
// 在这个函数内部先去判断这个标记是否被执行了
if(!done) {
// 如果没有被执行,把他的值设为true
done = true
// 并且调用fn
return fn.apply(this, arguments)
}
}
}
// 当我们调用这个函数,会创建一个局部变量,并返回一个函数,然后通过这个pay这个变量来指向这个函数
// 当外部作用域对内部函数有引用,那么once被执行后,并不会将done释放掉
let pay = once( function(money) {
console.log('支付: ${money} RMB')
})
// 当我们调用pay的时候, pay其实就是我们返回的命名函数,当我们调用内部函数,可以访问到外部的变量done
// 延长了外部函数在内部的变量作用范围
pay(5)
pay(5)
pay(5)
pay(5)
闭包的本质: 函数被执行的时候会被放到一个执行栈上,当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为外部引用不能被释放,因此内部函数依然可以访问外部函数的成员
4、闭包案例
- 求函数平方: Math.pow(4, 2) 、Math.pow(5, 2) , 需要求平方、三次方时候都需要传递第二个参数
// power是多少次幂的意思
function makePower (power) {
// 返回的是我们真正需要去计算的函数
// 需要接收一个数字,来求这个数字的平方、三次方
return function (number) {
// 返回计算的结果,需要传入两个参数, 一个是计算的数组,一个是多少次幂
// Math.pow(x, y)就是计算x的y次方
return Math.pow(number, power)
}
}
// 闭包: 我们在这个函数中返回了一个函数,将来要在这个函数中接收这个函数,并且在内部函数中访问了外部函数的局部变量,也就是这个参数power
// 求平方
//定义一个函数,来接收返回的函数
let power2 = makePower(2)
//求三次方
let power3 = makePower(3)
// 测试
// 调用函数,传入参数,打印得到的结果
console.log(power2(4))
console.log(power3(4))
// 这样调用函数,不需要再去指定2和3了
2、通过基本工资和绩效计算员工基本工资
// 假定getSalary是计算工资的函数,这里传入两个参数,基本工资(12000)和绩效工资(2000)
// 同一级别的员工基本工资相同,因为基本工资相应,所以需要不断的去重复
// getSalary(12000, 2000)
// getSalary(15000, 3000)
// getSalary(15000, 4000)
// 定义一个函数来计算员工的总工资(基本工资+绩效), 基本工资base
function makeSalary (base) {
// 返回一个函数,把基本工资和绩效工资相加, 绩效工资performance
return function (performance) {
// 将基本工资和绩效工资相加
return base + performance
}
}
// 定义一个函数生成级别是1和级别是2员工工资的计算函数
// 定义一个变量来记录生成的函数
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)
// 已经将工资的计算公式抽象出来了,并且员工级别和对应的工资也被定义,所以只需要知道员工当月的绩效工资,就能计算员工当月的总工资了
// 不需要再去调用员工的基本工资了
console.log(salaryLevel1(2000))
console.log(salaryLevel2(3000))
五、纯函数
- 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系), y=f(x)
1、同不纯函数的区别
数组的slice和splice分别是: 纯函数和不纯的函数
- slice返回数组中的指定部分,不会改变原数组
- splice对数组进行操作返回该数组, 会改变原数组
⑴、slice 纯函数
- 数组截取slice() 相当于字符串的substring()
- slice()的起止参数包括开始索引,不包括结束索引
// 定义一个数组
let array = [1, 2, 3, 4, 5]
// 截取数组中的前三个元素
// 从第一个元素开始, 第四个元素(3)是取不到的
console.log(array.slice(0, 3))
// => [1, 2, 3]
console.log(array.slice(0, 3))
// => [1, 2, 3]
console.log(array.slice(0, 3))
// => [1, 2, 3]
总结: 纯函数的定义–对相同的函数得到的结果也是相同的
⑵、splice 不纯函数
- splice()方法向/从数组中添加/删除元素,然后返回被删除的元素(是以数组的形式返回)
- splice()函数返回的是包含被删除元素的新数组
// 从第一个元素开始,截取3个元素(!!!这里的3是三个元素)
console.log(array.splice(0, 3))
// => [1, 2, 3]
console.log(array.splice(0, 3))
// => [4, 5]
console.log(array.splice(0, 3))
// => []
// 得到的三个数组不相同,因为他会修改原数组---当调用数组时,他会把这三个元素从数组中移除掉
// 多次调用的时候,根据相同的输入得到的输出是不同的,不是纯函数
// 自己写一个纯函数
function getSum (n1, n2) {
return n1 +n2
}
console.log(getSum(1, 2))
// => 3
console.log(getSum(1, 2))
// => 3
console.log(getSum(1, 2))
// => 3
// 根据相同的输入得到的是相同的输出,所以此处书写的是纯函数
- 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
- 我们可以把一个函数的执行结果交给另一个函数去处理
2、纯函数的好处
⑴、好处一
- 可缓存: 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
- 实例:有一个函数非常常用,每一次使用都需要等段时间才能得到结果,这对性能来说不好的
- 解决方案: 当这个函数第一次执行的时候,把这个结果缓存起来,当我们第二次调用的时候,直接从缓存中得到结果,从而提高性能
记忆函数: 牺牲内存,优化性能
// 引用lodash
const _ = require('lodash')
// 定义一个函数,求圆的面积
function getArea (r) {
// 为了看出来效果
console.log(r)
// 圆的面积是πr*2
// PI就是圆周率π,PI是弧度制的π,也就是180°
return Math.PI * r * r
}
// // 把计算圆面积的结果缓存下来
// // memoize 是会接收一个参数,他就是纯函数,会把处理的结果进行缓存; 并且memoize会返回一个带有记忆功能的函数
// let getAreaWithMemory = _.memoize(getArea)
// // 参数为半径4
// console.log(getAreaWithMemory(4))
// // => 4
// // => 50.26548245743669
// console.log(getAreaWithMemory(4))
// // => 50.26548245743669
// console.log(getAreaWithMemory(4))
// // => 50.26548245743669
// 模拟 memoize 方法的实现
// 定义一个memoize函数,里面传入了一个 f 函数
function memoize (f) {
// 需要定义一个对象, 把对象的执行结果存储起来,下次调用这个函数的时候,需要把他层级缓存的结果获取到
// 因为是纯函数,输入等于输出,所以可以把函数的参数作为对象的栈,把函数的执行结果作为对象的值
// 定义一个对象
let cache ={}
return function () {
// 在这个函数中,需要判断cache是否含有(f)这个执行结果(根据(f)这个参数,来看一下cache的执行结果),获取到了的话,直接返回, 没有取到的话需要调用这个函数,并且把结果缓存起来,把执行结果返回
// cache的参数,只需要通过返回函数中的参数来获取就OK了
// let key = arguments
// arguments是一个伪数组,需要把它转化一个字符串,把他作为对象的栈
let key = JSON.stringify(arguments)
// 判断是否有返回结果,如果有直接把缓存结果取出来,没有的话需要执行(f)这个函数
// cache[key] = cache[key] || f
// 调动(f)函数时,需要把arguments传递给(f)这个函数,但是arguments是一个伪数组,里面可能有一个或者多个值
// apply可以改变函数中的类子,他的第二个参数可以把函数展开,把每一项传递给函数,我们的不是就是为了把函数展开,把每一项传递给函数
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
// 测试
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
// => 4
// => 50.26548245743669
console.log(getAreaWithMemory(4))
// => 50.26548245743669
console.log(getAreaWithMemory(4))
// => 50.26548245743669
⑵、好处二
可测试: 纯函数让测试更方便
⑶、好处三
并行处理:
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以再并行环境下可以任意运行纯函数(Web Worker)
3、纯函数的副作用
对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯
let mini = 18
function checkAge (age) {
// 判断用户输入的年龄是否大于变量值18
return age >= mini
}
// 当传入的age数值>=18,输出的值为true;当变量值mini数值改变,传入的age数值>=18,输出的值不一定是true;所以是不纯函数
// 变为纯函数: 只需要把全局变量mini放在函数内部,变为局部变量
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
// 变量的值等于一个数值,有硬编码
let mini = 18
return age >= mini
}
- 副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
- 副作用来源: 全局变量(如上例)、配置文件、数据库、获取用户的输入…
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降,不合适扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生
六、Lodash
- lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
- 现代化实用的js库,提供了模块化、高性能以及附加功能
1、安装Lodash
// 安装lodash
// 第一步: 初始化package.json
// $ npm init -y
// 第二步: 安装lodash
// $ npm i lodash
2、演示lodash
first / last / toUpper / reverse / each / includes / find / findIndex
// 引用lodash
const _ = require('lodash')
// 定义一个数组
const array = ['jack', 'tom', 'lucy', 'kate']
// first === head 获取数组中的第一个元素
console.log(_.first(array))
// => jack
console.log(_.last(array))
// => kate
// 输入即输出,是纯函数
// toUpper 将字符串中的字母大写
// 将数组中的第一个字母大写
console.log(_.toUpper(_.first(array)))
// => JACK
// reverse 倒序输出
console.log(_.reverse(array))
// => ['kate', 'lucy', 'tom', 'jack']
// 改变了原始数组,不是纯函数
// each === forEach 遍历数组
// 两个参数: 一个是数组,一个是回调函数(第一个是元素,第二个是元素的索引)
// _.each(array, (item, index)) => {
// 接收each的返回值
let r = _.each(array, (item, index) => {
console.log(item, index)
// => kate 0
// => lucy 1
// => tom 2
// => jack 3
})
console.log(r)
// => [ 'kate', 'lucy', 'tom', 'jack' ]
3、Lodash中的柯里化方法
_.curry(func)
- 功能: 创建一个函数,该函数接收一个或多个func的参数, 如果func所需要的参数都被提供,则执行func并返回执行结果,否则继续返回该函数并等待接收剩余的参数
- 参数: 需要柯里化的函数
- 返回值: 柯里化后的函数
// lodash 中的curry基本使用
const _ = require('Lodash')
// 创建一个基本函数
// 三个参数就是三元函数,柯里化可以把多元函数转化为一元函数,对之后的函数组合是非常重要的
function getSum (a, b, c) {
return a + b + c
}
// 接收一个函数
const curried = _.curry(getSum)
// 测试
// 如果调用curried的时候,传递了getSum所需要的所有参数,那么会立即被调用执行,并返回所需要的结果
console.log(curried(1, 2, 3))
// => 6
// 如果给curried传递参数的时候,只传递了getSum中的部分参数,那么他会返回一个函数,等待接收getSum中的其余参数
console.log(curried(1)(2, 3))
// => 6
// 可以把一个任意多元的函数转化为多个一元函数
console.log(curried(1, 2)(3))
// => 6
4、Lodash中的组合函数
- Lodash中组合函数flow()或者flowRight(),他们都可以组合多个函数
- flow()是从左到右运行
- flowRight()是从右到左运行,使用的更多一些
lodash中函数的组合方法 _.flowRight()
// 导入lodash
const _ = require('lodash')
// 通过箭头函数创建会使代码更简洁
// 定义一个reverse函数,需要传入一个arr数据,并调用数组的reverse方法并把结果返回
// 实现翻转
const reverse = arr => arr.reverse()
// 实现获取第一个元素
// 定义一个first函数,也需要传入一个arr数据,直接返回数组中的第一个元素
const first = arr => arr[0]
// 字符串转换成大写
// 定义一个toUpper函数,传入一个字符串,并调用字符串的toUpperCase这个方法
const toUpper = s => s.toUpperCase()
// 通过 _.flowRight(和之前的compose功能相同,只不过他可以传递多个参数)将多个函数组合起来,生成一个新的函数
// 将字符串翻转, 再调用第一个元素, 最后再将元素转化成大写
const f = _.flowRight(toUpper, first, reverse)
// 测试
console.log(f(['one', 'two', 'three']))
// => THREE
5、Lodash-fp模块
- lodash的fp模块提供了实用的对函数式编程友好的方法
- !!!提供了不可变 auto-curried iteratee-first data-last的方法(这些友好的方法是不可变的,是柯里化的; 如果一个方法的参数是函数的话,那就要求函数优先,数据滞后)
⑴、lodash 模块
const _ = require('lodash')
// map方法
// 作用: 对数组进行遍历,并且遍历的过程中,可以指定一个函数,对数组中的每一个元素进行处理
// 当我们调用lodash中的map方法时,它要求我们数据优先,函数滞后
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
// 当我们只传递部分参数的时候, 没有对函数进行处理, 那么他会原封不动的返回这个数组
_map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
// split方法
// 作用: 对字符串进行切割
// 当我们调用lodash中的split方法时,先数据也就是字符串,再传入分隔符
_.split('Hello World', '')
⑵、lodash/fp 模块
const fp = require('lodash/fp')
// lodash/fp模块中是函数优先,数据滞后
fp.map(fp.toUpper), ['a', 'b', 'c']
// 如果只传递了一个参数,那么会返回一个函数,这个柯里化过的函数
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
⑶、案例 ( NEVER SAY DIE - - > never-say-die )
lodash 的 模块
const _ = require('lodash')
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(join('-'), map(_.toLower), split(' '))
console.log(f('NEVER SAY DIE'))
// => never-say-die
lodash中的方法需要重新创建是因为: 都是方法有限,数据滞后,而且没有被柯里化
lodash 的 fp 模块
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
// => never-say-die
6、Lodash-map方法的小问题
Lodash 和 Lodash/fp 模块中 map 方法的区别
lodash方法: 将数组中的所有元素都转化成整数
const _ = require('lodash')
// lodash中的map,是数据有限,函数滞后; 先传入数组,再传入一个指定函数,对数组中的每一个元素进行处理
// parseInt就是转化成整数的方法
console.log(_.map(['23', '8', '10'], parseInt))
// => [ 23, NaN, 2 ]
与预期结果不符
map会把数组中的每一个元素传递给parseInt进行处理,把parseInt的执行过程展开
- 传入’23’时, parseInt(‘23’, 0, array),此处的0是索引值,是map自动传递过来的
- 传入’8’时, parseInt(‘8’, 1, array),此处的1是索引值,是map自动传递过来的
- 传入’10’时, parseInt(‘10’, 2, array),此处的2是索引值,是map自动传递过来的
parseInt的第二个参数,代表转化为几进制,最后会将数组中的元素转化为几进制的数值
- 传入’23’时, ‘0’代表10进制, 所以得到’23’
- 传入’8’时, ‘1’根本不支持,所以是’NaN’
- 传入’10’时, ‘2’待办2进制,所以得到’2’
解决办法: 封装我们需要的parseInt,只传递一个值,就解决了这个问题
lodash/fp方法: 将数组中的所有元素转化成整数
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10']))
// => [ 23, 8, 10 ]
console.log(fp.map(parseInt, ['23.5', '8.5', '10']))
// => [ 23, 8, 10 ]
lodash 和 lodash/fp 中的函数接收的参数不一样,lodash中的parseInt接收三个参数: 要处理的元素、索引、数组; 而lodash/fp的函数只接收一个参数,就是需要处理的元素
七、柯里化(Haskell Brooks Curry)
什么是柯里化: 当函数有多个参数的时候,可以对他进行改造,可以调用一个函数,只传递部分参数,并且让这个函数只传递部分参数,这个函数可以接收剩余的参数,并且返回想要的结果
- 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余的参数,返回结果
1、纯函数副作用中硬编码的问题
问题:
function checkAge (age) {
// 硬编码: 基本值绑定了一个数字
let min = 18
return age >= min
}
解决办法: 局部变量,提取到参数变量
// 函数中传入两个参数,一个是基础值,一个是比较值
function checkAge (min, age) {
return age >= min
}
// 得到了编码之后的纯函数
// 测试
console.log(checkAge(18, 20))
// => true
console.log(checkAge(18, 24))
// => true
console.log(checkAge(20, 20))
// => true
柯里化
// 因为18可能会被经常使用,使用避免重复,我们使用闭包的原理避免重复
/* function checkAge (min) {
return function (age) {
return age >= min
}
} */
// ES6箭头函数
// 定义一个变量checkAge,他就等于一个箭头函数,箭头函数需要传递一个参数,也就是基本值min,并且他内部需要返回一个函数,我们就用()把他括起来,并且返回的函数也需要传递一个参数age,并且返回的值需要将基本值和年龄值进行比较(age >= min),如果箭头函数中只有一句代码,那就类似我们把它return过来了
let checkAge = min => (age => age >= min)
// 定义一个checkAge18,当我们调用checkAge函数的时候,传入18之后,他会返回一个新的函数,这个新的函数就会返回到checkAge18这个变量里面来,将来我们调用checkAge18这个函数的时候,这个基本值min就始终是18
// 这里用到了闭包和高阶函数
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
// 测试
// 调用值和基本值进行比较
console.log(checkAge18(20))
// => true
console.log(checkAge18(24))
// => true
2、柯里化案例
字符串的match方法: 可以传递正则表达式
// !!此处可能存在问题
// ''.match(//)
// 匹配空白字符
''.match(/\s/)
// 任意个空白字符
''.match(/\s+/)
// 全局里配
''.match(/\s+/g)
// 这是要去匹配或提取字符串中的所有空白字符
''.match(/\s+/g)
// 提取字符串中的所有数字
上面是使用面向过程的方式提取字符串中的字符或者数字,下面是函数式编程,来提取或者匹配字符串中的指定内容
// 引用lodash
const _ = require('lodash')
/* // 封装一个match的纯函数
// 需要传入两个参数,一个是正则表达式匹配我们需要传递的内容,另一个是字符串
function match (reg, str) {
// 直接调用并直接返回字符串match方法的结果
return str.match(reg)
}
// 如果要经常调用match函数,那这个正则表达式是不断重复的
// 利用柯里化让一个函数生成一个新的函数 */
// 调用lodash中的curry方法
// _.curry()
// lodash中的curry需要一个参数,并且他会返回一个函数,所以还需要定义一个变量来接收他返回的函数,这样就需要两个变量名称,这样不太方便;所以我们把这个函数直接传递给curry
// 在定义一个变量,接收curry返回回来的结果
const match = _.curry(function (reg, str) {
return str.match(reg)
})
// 定义了一个curry方法,返回了一个柯里化的函数
// match方法可以 只传递两个参数,也可以只传递一个参数,来生成一个新的函数
// 调用match函数,判断字符串中是否有空白字符
const haveSpace = match(/\s+/g)
// 测试
console.log(haveSpace('hello Word'))
// => [ ' ' ]
console.log(haveSpace('helloWord'))
// => null
// 调用match函数,判断字符串中是否有数字
const haveNumber = match(/\d+/g)
// 测试
console.log(haveNumber('123abc'))
// => [ '123' ]
console.log(haveNumber('abc'))
// => null
过滤数组中的空白字符
// 第一个参数是如何对我们的数组进行过滤,也就是我们的回调函数func,第二个就是数组
const filter = _.curry(function(func, array) {
// 调用filter方法,传入一个回调函数func
return array.filter(func)
})
// 测试
// 过滤含有空白字符数组的元素
console.log(filter(haveSpace, ['John Conner', 'John_Donne']))
// => [ 'John Conner' ]
// filter会帮我们生成一个新的函数findSpace
const findSpace = filter(haveSpace)
// 测试
console.log(findSpace(['John Conner', 'John_Donne']))
// => [ 'John Conner' ]
3、柯里化原理模拟
const _ = require('Lodash')
function getSum (a, b, c) {
return a + b + c
}
// 当我们调用curry方法的时候,需要给他传递一个参数,这个参数就是纯函数getSum,当调用完成之后会返回一个函数,这个函数是柯里化之后的函数curried
const curried = _.curry(getSum)
// 调用返回的柯里化函数,我们有两种形式,第一种就是getSum这个函数有几个参数,在curried调用时候我们就给他几个参数,如果传递的参数与getSum的参数个数相同,那么就会立即调用getSum,并且返回他的执行结果;
console.log(curried(1, 2, 3))
// => 6
// 第二种就是当我们调用getSum的时候,只传递getSum需要的部分参数,此时curried函数会返回一个新的函数,并等待getSum去接收所需要的其他参数
console.log(curried(1)(2, 3))
// => 6
console.log(curried(1, 2)(3))
// => 6
使用自己创建的curry函数来实现lodash中的curry方法
// const _ = require('Lodash')
function getSum (a, b, c) {
return a + b + c
}
// // const curried = _.curry(getSum)
// // 为了去掉lodash中curry方法,测试我们自己创建的curry函数
const curried = curry(getSum)
// // => 6
console.log(curried(1)(2, 3))
// // => 6
console.log(curried(1, 2)(3))
// // => 6
// 根据上面的实现方法同理写出
// 当我们调用curry方法的时候,需要给他传递一个参数,这个参数就是纯函数func
function curry (func) {
// ES6剩余参数的写法 ...,定义参数的名字args
// return function (...args) {
// 创建一个柯里化函数 curriedFn
return function curriedFn(...args) {
// 对比一下实参个数和形参个数是否相同: args是一个数组,args.length可以获取实际调用参数的个数;形参个数是getSun传入的形参个数 'a + b +c',可以通过函数名.length来获取
// 而这个getSum其实就是curry当前的这个参数func
if (args.length < func.length) {
// 返回一个新的函数
return function () {
// 需要将第一次传递的参数和第二次传递的参数加起来一起传递给curriedFn
// args是第一个数组,concat是和并两个数组
// return curried(args.concat(arguments))
// arguments是一个伪数组,需要转化成真数组
// ...是将数组展开(同上面'(1)(2, 3)'这种形式)
return curried(...args.concat(Array.from(arguments)))
}
}
// 形式参数 >= 时间参数时,就会立即调用func,并且返回他的执行结果
// '...args'其实是一个数组,我们需要把数组展开,传递给这个func; 两个方法: 一个是ES6中apply方法,另一个是...这个方法
return func(...args)
}
}
// 测试
// 不再使用lodash中的curry方法,而是使用自己创建的curry函数
// 将lodash引用去掉 + lodash中引用curry方法去掉
4、总结
- 柯里化可以让我们给一个函数传递较少的参数, 得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的’缓存’(闭包)
- 让函数更灵活,让函数粒度更小(因为柯里化可以生成粒度更小的函数),目的是为了之后组合的时候使用
- 可以把多元函数转化为一元函数(多元就是多个参数,一元就是一个参数),可以组合使用函数产生强大的功能
八、组合函数
- 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
1、洋葱代码
洋葱代码: 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
获得数组的最后一个元素再转换成大写字母
// 先通过reverse(array)对数组进行翻转,然后通过first获取数组中的第一个元素,最后再用toUpper将我们的元素转化成大写
_.toUpper(_.first(_.reverse(array)))
- 一层包裹着一层,就是洋葱代码
- 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
2、管道
- 下面这张图表示程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b,可以想象a数据通过一个管道(fn)得到了b数据
- a => fn => b
- 如果管道fn过长,中间出现’漏水’等情况,不容易对中间进行检查问题,所以需要对管道fn进行拆分
- 下面这张图可以想象成把fn这个管道拆分成了3个管道f1、f2、f3,数据a通过管道f3得到结果m,m再通过管道f2得到结果n, n再通过管道f1得到最终结果b
- a => f3 => m => f2 => n => f1 => b
3、语法演示
定义一个组合函数: 这个函数能够接受多个函数类型的参数,并且能够把他们组合成一个新的函数返回
// 输入两个函数
function compose (f, g) {
// 返回的函数要能接收一个参数
// 函数就相当于一个管道,输入一个参数,并且输入完成之后需要返回一个相应完成的结果
return function(value) {
// 处理这个输入,并且把处理的结果返回
// 从右到左执行,其实洋葱代码并没有减少,只是把他封装起来了
return f(g(value))
}
}
实例: 通过函数组合来获取数组中的最后一个元素
// 原理:先将数组进行反转,再获取数组中第一个元素
// 先定义一个反转数据的函数
function reverse (array) {
return array.reverse()
}
// 获取数组中的第一个元素
function first (array) {
return array[0]
}
// 通过compose将两个函数组合起来,生成一个新的函数last
// 从右到左执行,先翻转reverse,再获取first
const last = compose(first, reverse)
// 调用last,last需要一个参数value,也就是数组的形式
console.log(last([1, 2, 3, 4]))
// => 4
4、组合函数原理模拟
模拟lodash中的flowRight
方法一: 使用Lodash中的flowRight
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
// 删掉lodash中引用flowRight的方法,引用我们自己创建的compose的方法
// const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
// => THREE
方法二: 自己创建compose方法,替代lodash中的flowRight方法
// const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// const f = _.flowRight(toUpper, first, reverse)
// 删掉lodash中引用flowRight的方法,引用我们自己创建的compose的方法
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
// => THREE
// flowRight的参数是不固定的,并且参数都是纯函数,调用完flowRight之后会帮我们生产一个新的函数,这个新的函数需要接收一个参数,最终他需要对参数进行处理,处理的流程是reverse => first => toUpper
// 定义一个组合函数compose, 组合函数的参数不固定,所以用剩余参数来写(...args)
function compose (...args) {
// 当我们调用函数的时候,他会返回一个函数,并且需要传入一个参数
return function (value) {
// 最终需要将我们的结果返回回来
// 因为是从右到左调用的,所以先对数组进行翻转
// reduce是对数组中的每一个元素去执行一个由我们提供的函数,并将其汇总成一个单个的结果
// reduce中需要两个参数,第一个是我们汇总的结果,第二个是如何处理我们每一次返回的这个结果,并返回一个新的值
// 此处的fn就是数组中的每一个值,我们通过这个函数来处理传入参数,处理完成之后将他返回
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
// 我们期望第一次调用的初始值是传递进来的value,那我们可以在reduce第二个参数的位置传入这个初始值
}, value)
}
}
// 测试
// 将lodash引用删掉 + 删掉lodash中引用flowRight 引用我们自己创建的compose方法
方法三: 理解刚刚实现的原理,通过箭头函数来简化
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// 定义一个compose函数,这个函数需要一个参数'...args';这个函数会返回一个函数,返回的函数需要一个参数'value',这个函数里面又要调用我们这个函数的形式'args.reverse().reduce()',reduce中依然需要一个参数,这个参数依然是一个函数,所以可以用箭头函数来写'acc(我们汇总的结果),fn(如何去处理这个结果)',最后返回fn去处理的这个结果,最后acc需要一个处理的初始值
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
5、函数组合-结合律
- 结合律(associativity)
- let f = compose(f, g, h)
- let associativity = compose(compose(f, g), h) == compose(f, compose(g, h))
语法演示
// 函数组合要满足结合律
const _ = require('lodash')
// const reverse = arr => arr.reverse()
// const first = arr => arr[0]
// const toUpper = s => s.toUpperCase()
// 其实不用自己写,lodash中已经帮我们写好了我们需要的方法
// const f = _.flowRight(toUpper, first, reverse)
// // lodash中的好处,可以直接使用lodash中给我们提供的纯函数
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// // 先让前两个函数组合起来,再加上第三个函数
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
// 先让后两个函数组合起来,再加上第一个函数
const f = _.flowRight( _.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one', 'two', 'three']))
// => THREE
6、函数组合-调试
⑴、实例: NEVER SAY DIE - - > never-say-die
// 实现原理: 先使用空格对字符串进行切割,再将大写转化成小写,最后使用小横线对字符串进行连接
// 导入lodash
const _ = require('lodash')
// 使用lodash中的split方法对数组进行切割
// _.split()
// _.split含有多个参数,所以再函数组合的时候没有办法直接使用
// 第一个参数是分隔符,第二个参数是数据
// const split = (sep, str)
// 这里的split还是包含了多个参数,需要将它转化成一个参数
//利用函数的柯里化,将多元函数转化成一元函数
const split = _.curry((sep, str) => _.split(str, sep))
// _.toLower()
// _.toLower()将字符串中的元素转化成小写
// 本身就是一元函数,可以直接调用
_.join()
// _.join() 将元素用分隔符组合起来
// !!!在函数组合的时候,只能用一个参数的函数; 数组我们是最后给他传递,这个参数的位置也要给他交换一下
// _.curry中首先传递的是分隔符,然后传递的是数组
const join = _.curry((sep, array) => _.join(array, sep))
// 将刚刚准备好的函数组合起来: 首先使用 ''(空格)将字符串切割(柯里化可以只传递部分参数), 然后将切割后的结果转化成小写的形式, 最后使用短横线把这个内容组合起来
const f = _.flowRight(join('-'), _.toLower, split(' '))
console.log(f('NEVER SAY DIE'))
// => n-e-v-e-r-,-s-a-y-,-d-i-e
和我们预期的结果不符,需要对函数进行调试
⑵、调试split管道
const _ = require('lodash')
// !!!log函数需要先定义,再使用
// 定义一个log函数,需要传入一个参数,这个参数就是第一个函数执行完毕的结果
const log = v => {
// 先将执行完毕的结果打印出来
console.log(v)
// 将打印的结果返回个下一个函数
}
//利用函数的柯里化,将多元函数转化成一元函数
const split = _.curry((sep, str) => _.split(str, sep))
// _.toLower()
// _.toLower()将字符串中的元素转化成小写
// 本身就是一元函数,可以直接调用
_.join()
// _.join() 将元素用分隔符组合起来
// !!!在函数组合的时候,只能用一个参数的函数; 数组我们是最后给他传递,这个参数的位置也要给他交换一下
// _.curry中首先传递的是分隔符,然后传递的是数组
const join = _.curry((sep, array) => _.join(array, sep))
// 和我们预期的结果不符,需要对函数进行调试
// 调试的原理: 在函数的组合过程中,当第一个函数执行完毕之后,会把执行完的结果传递给下一个函数,交给下一个函数继续去执行,所以我们可以在第一个函数的后面增加一个函数,将第一个函数执行完毕的结果打印出来, 并且把打印的结果返回给下一个函数
// 调试
const f = _.flowRight(join('-'), _.toLower, log, split(' '))
// 调用f函数,将参数传递进来
console.log(f('NEVER SAY DIE'))
// 调试split
// => [ 'NEVER', 'SAY', 'DIE' ]
符合预期,需要对函数继续进行调试
⑶、调试toLower管道
// 调试
// const f = _.flowRight(join('-'), _.toLower, log, split(' '))
// 调用f函数,将参数传递进来
// console.log(f('NEVER SAY DIE'))
// 调试split
// => [ 'NEVER', 'SAY', 'DIE' ]
const f = _.flowRight(join('-'), log, _.toLower, split(' '))
// 检查toLower
console.log(f('NEVER SAY DIE'))
// 调试toLower, 发现了问题: 将数组形式转换成了字符串的形式
// => never,say,die
⑷、解决办法
// 导入lodash
const _ = require('lodash')
//利用函数的柯里化,将多元函数转化成一元函数
const split = _.curry((sep, str) => _.split(str, sep))
// _.toLower()
// _.toLower()将字符串中的元素转化成小写
// 本身就是一元函数,可以直接调用
_.join()
// _.join() 将元素用分隔符组合起来
// !!!在函数组合的时候,只能用一个参数的函数; 数组我们是最后给他传递,这个参数的位置也要给他交换一下
// _.curry中首先传递的是分隔符,然后传递的是数组
const join = _.curry((sep, array) => _.join(array, sep))
// map
// 传入的两个参数,首先是需要传入一个回调函数fn,它里面决定,如何去处理数组中的每一项
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
console.log(f('NEVER SAY DIE'))
// => [ 'NEVER', 'SAY', 'DIE' ]
// => []
⑸、优化调试方法
// 导入lodash
const _ = require('lodash')
// 定义一个函数trace, 传入两个参数, 第一个参数tag是标记, 第二个是实际的值
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
//利用函数的柯里化,将多元函数转化成一元函数
const split = _.curry((sep, str) => _.split(str, sep))
_.join()
// _.join() 将元素用分隔符组合起来
// !!!在函数组合的时候,只能用一个参数的函数; 数组我们是最后给他传递,这个参数的位置也要给他交换一下
// _.curry中首先传递的是分隔符,然后传递的是数组
const join = _.curry((sep, array) => _.join(array, sep))
// map
// 传入的两个参数,首先是需要传入一个回调函数fn,它里面决定,如何去处理数组中的每一项
const map = _.curry((fn, array) => _.map(array, fn))
// 如果在调试过程中,log用的次数较多,那么他返回的内容难以对应,不够清晰,所以需要对log进行重新改造
// const f = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
// console.log(f('NEVER SAY DIE'))
// => [ 'NEVER', 'SAY', 'DIE' ]
// => []
// trace需要两个参数,第一个是打印的哪个函数,第二个是打印的数据,有多个参数,所以需要柯里化来实现
// !!!将log改造成trace(在上方定义)
// 测试
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))
// => split 之后 [ 'NEVER', 'SAY', 'DIE' ]
// => map 之后 [ 'never', 'say', 'die' ]
// => never-say-die
7、Point Free
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前,我们需要定义一些辅助的基本运算函数 — 组合函数
⑴、概念
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
⑵、案例一: Hello World => Hello_world**
非Point Free 模式
// 定义一个函数f,接收我们需要处理的数据word
function f (word) {
// 对我们的数据进行处理,并得到想要的结果
return word.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'))
Point Free 模式
// 实现原理: 先将大写字符转化成小写,再把空格替换成下划线(如果中间的空格比较多,需要用到正则表达式把这些空格匹配到,把他们替换成下划线)
const fp = require('lodash/fp')
// 组合成一个新的函数,这时候用到函数组合flowRight方法
// const f = fp.flowRight()
// 先将字符串大写变小写,再把空格替换,fp是从右到左执行的
// const f = fp.flowRight(fp.replace, fp.toLower)
// 通过查看replace()方法就可以查看第一个参数是replace的模式,可以是正则表达式; 第二个参数是替换的内容, 第三个参数是字符串,因为是柯里化函数,所以只需要传递部分参数(第一个是正则表达式,用来匹配空白; 第二个参数是替换后的下划线)
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
// 测试
console.log(f('Hello World'))
// => hello_world
⑶、案例二: world wild web => W. W. W
// 实现原理: 首先将字符串用空格进行切割,转化成数组,再将数组中的元素转化成大写,提取元素的首字母,最后再用 . 把他们组合起来
const fp = require('lodash/fp')
// 定义一个组合函数,组合函数的方法为flowRight
// 先用空格对字符串进行切割,再遍历数组中的每一个元素,对元素转化成大写, 再遍历数组中的每一个元素, 提取每一个元素的第一个字母,再用 . 组合到一起
// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper),fp.split(' '))
// 测试
// console.log(firstLetterToUpper('world wild web'))
// => W. W. W
// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper),fp.split(' '))
// map会对数组进行遍历,这种方法会对数组进行两次遍历,这时的性能是比较低的
// 通过flowRight将first和toUpper组合起来
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)),fp.split(' '))
// 测试
console.log(firstLetterToUpper('world wild web'))
// => W. W. W
九、函子
1、函子的概念
⑴、为什么要学习函子
到目前为止,已经学习了函数式编程的一些基础,但我们还没有演示在函数式编程,如何把副作用控制在可控的范围内、异常处理、异步操作等
⑵、什么是functo
- 容器: 包含值和值的变形关系(这个变形关系就是函数)
- 是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
⑶、描述函子
函子是一个普通的对象,这个对象需要维护一个值,并且要对外公布一个map方法, 所以可以通过一个类来描述函子
// 定义一个类,叫Container(容器)
class Container {
// 定义一个构造函数
// 当我们创建函子的时候,函子的内部要有一个值,所以在构造函数的时候要把这个值传递进来
constructor (value) {
// 函子的内部要把这个值存储起来,这个值是函子里面的,他是不对外公布的
// 内定所有以下划线开头的成员都是私有成员
this._value = value
}
// 盒子需要对外公布一个map方法,盒子的作用是接收一个处理值的函数,这个函数也是一个纯函数,因为我们要把value这个值传递给这个函数,由这个函数来真正处理我们这个值
map (fn) {
// 在map中需要去处理这个值,并且最终还要返回一个新的盒子(函子)
// 返回函子的时候,要把处理的值传递给Container
return new Container(fn(this._value))
}
}
// 创建一个函子对象
// 通过一个变量接收一下
let r = new Container(5)
// 处理函子的值,让处理的值 +1 返回
.map(x => x + 1)
// 在map方法里面我们调用了fn这个函数,并把这个值传递进来,并且他把新的结果返回了一个函子对象, 因为他返回了一个函子对象,所以我们可以继续对这个函子进行处理
.map(x => x * x)
console.log(r)
// => Container { _value: 36 }
// 5 + 1 => 6; 6 * 6 => 36
- map返回的不是指,而是一个新的函子对象,在这个对象里面去保存新的值
- 我们始终不把值对外公布,需要去处理值的时候,需要给map传递一个处理值的函数
我们每一个创建函子的时候,都需要用new进行处理,非常不方便,所以需要进行封装一下
class Container {
// 在static中创建一个静态方法,这个方法of的作用就是给我们返回一个函子对象,创建函子对象的时候需要传入一个value,of方法就需要接收一个value
static of (value) {
// 在of方法中直接new Container,把value传递进来
// of里面就封装了new这个关键字
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
// return new Container(fn(this._value))
// 因为他是一个静态方法,所以可以通过类名来调用
return Container.of(fn(this._value))
}
}
// 测试
// 直接调用Container.of方法
let r = Container.of(5)
// 在map方法里面传入如何去处理传递的这个值
.map(x => x + 1)
// 继续去处理上一个函子对象返回的map的值
.map(x => x * x)
console.log(r)
// => Container { _value: 36 }
// 5 + 1 => 6; 6 * 6 => 36
- 我们最终拿到的不是一个值,而是一个函子对象,我们的值在这个函子对象里面,我们的值始终在这个盒子里面
- 我们永远不用把这个值取出来,我们想要处理这个值就用map方法,想要把这个值打印出来,就可以通过map方法传递的这个值里面console.log把这个值打印出来
⑷、总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里面封装了一个值
- 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),他需要一个参数并且返回一个值,把处理值的过程交给函数来完成
- 最终map方法返回一个包含新值的盒子(函子),所以可以通过.map进行链式调用,因为map方法始终返回的是一个函子,所有的函子都有map方法,因为我们可以把不同的运算方法封装到函子中,所以我们可以延伸出不同类型的函子,那我们有多少运算就有多少函子,最终我们可以通过不同的函子解决实际问题
2、语法演示
// 传递 null undefined的问题
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 + 1)
// .map(x => x * x)
// console.log(r)
//传入 null
Container.of(null)
.map(x => x.toUpperCase())
// null.toUpperCase是会报错的,此时也让'x => x.toUpperCase'这个函数变得不纯,因为纯函数是相同的输入有相同的输出,而输入的null的时候,是没有输出,而且出现了异常,此时的这个null就是我们的副作用
3、MayBe函子
- 我们在编程的过程中,可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围内)
// 创建一个类MayBe
class MayBe {
// 创建MayBe函子的时候,需要new这个类,为了让外部更方便的创建,我们需要一个静态方法of,我们把new这个过程封装到of里面来
// 接收一个value初始化这个构造函数
static of (value) {
return new MayBe(value)
}
// 在这个类里面需要创建一个构造函数,这个构造函数需要接收一个值
constructor (value) {
// 在这个函数里面,需要设置一个属性接收这个值,把这个值保存起来
this._value = value
}
// 定义一个map方法,map方法是用来接收我们这个函数,并处理这个值,并返回一个新的函子
map (fn) {
// return MayBe.of(fn(this._value))
// 如果这个值是空的话,那么直接返回一个函子,这个函子的值是null; 如果有值的话,就传入fn调用this.value
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
// 问题: 如果函数 this._value === null, 那么函数就会出现异常
// 解决方法: 创建一个辅助方法,判断 this.value 是否为空
isNothing () {
return this._value === null || this._value === undefined
}
// !!!创建好辅助方法之后,需要在调用map方法之前做一个判断
}
// 测试
let r = MayBe.of('Hello World')
// 传入一个函数,去处理我们函子内部的数据
.map(x => x.toUpperCase())
console.log(r)
// => MayBe { _value: 'HELLO WORLD' }
let v = MayBe.of(null)
// 传入一个函数,去处理我们函子内部的数据
.map(x => x.toUpperCase())
console.log(v)
// => MayBe { _value: null }
// MayBe方法的问题
let f = MayBe.of("Hello World")
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(f)
// => MayBe { _value: null }
使用MayBe虽然会解决当传入的值为空,会返回空值,但是不知道是哪个流程导致的空值
4、Either函子
- Either 两者中的任何一个,类似if…else的处理
- 异常会让函数变得不纯,either函子可以用来做异常处理
const { xor } = require("lodash")
class Left {
//为了创建函子方便,我们可以给他定义一个静态的方法of
static of (value) {
// 在of方法里面,直接调用这个函数,返回一个对象
return new Left(value)
}
// 因为left也是一个函子,所以需要定义一个构造函数,在函数中传入一个value
constructor (value) {
// 在函子中通过一个属性把值记录下来
this._value = value
}
// 实现map方法,并传递一个函数
map (fn) {
// 这个map里面,比较特殊,我们直接返回这个this(这个对象)
// 这个类怎么使用,待会儿展示
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
// 在这里我们需要right方法,返回一个新的函子的值; 这个新的函子的值是我们通过fn处理之后的结果
return Right.of(fn(this._value))
}
}
// 通过right来创建一个函子,输入一个值12;调用map方法,在map方法中,我们要传递一个函数,这个函数要用来处理函数内部的值
let r1 = Right.of(12).map(x => x + 2)
// 定义一个变量接收left创建的这个函子
let r2 = Left.of(12).map(x => x + 2)
// 测试
// console.log(r1)
// // => Right { _value: 14 }
// // 在map方法中,直接将this(这个对象)返回来,并没有执行这个fn
// console.log(r2)
// // => Left { _value: 12 }
// 为什么要这么做? 嵌入一个错误消息,把一个json对象转化成一个json字符串
// 定义一个函数,他里面传入一个字符串
function parseJSON(str) {
// 接下来把传入的JSON转化成字符串,并且返回
// 因为创建JSON.parse的时候可能会出现异常,所以给他加上try catch
try {
// 我们会把我们处理的结果交给这个函子,将来在这个函子的内部去处理
return Right.of(JSON.parse(str))
// 传入错误项
} catch (e) {
// 如果发生异常不进行处理的话,就不是纯函数
// 传递错误信息,把这个错误信息记录下来
return Left.of({error: e.message})
}
}
// 测试
// 定义一个变量,接收parseJSON返回的函子
// 传入一个非法的值,这个name没有加双引号 ""
let r = parseJSON('{ name: zr }')
console.log(r)
// => Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
// 传入一个合法的值
let v = parseJSON('{ "name": "zr" }')
console.log(v)
// => Right { _value: { name: 'zr' } }
// 通过map方法将数值转换成大写
let b = parseJSON('{ "name": "zr" }')
.map(x => x.name.toUpperCase())
console.log(b)
// => Right { _value: 'ZR' }
5、IO函子
- IO函子中的_value始终是一个函数,这里是把函数作为值来处理
- IO函子可以把不纯的动作储存到_value中(value中存储的是函数,在函子内部并没有调用这个函数),延迟执行这个不纯的操作(惰性执行),包装当前的操作(纯操作)
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
// 首先先创建一个IO的类
class IO {
// 为了创建IO方便,这里创建了一个静态的of方法
// 这个of方法和之前的是不一样的,这个x接收的是一个数据,不是一个函数
static of (x) {
// of里面我们会返回一个IO的函子,调用一个构造函数, 当我们调用构造函数的时候,我们传递的是一个函数
return new IO (function() {
// 这个函数,其实将我们传递的值包裹起来了
return x
})
}
// 通过of方法,我们能感受到,IO函子最终其实还是想把值给我们返回,只不过IO函子通过一个函数(return x),把这个值包裹起来了
// 然后创建一个构造函数
// 构造函数接收一个函数,这个是和之前不一样的
constructor (fn) {
通过value将函数fn存储起来
this._value = fn
}
// IO函子的value保存的是fn这个函数,而这个函数(fn)返回的是一个值
// 他把这个值做了延迟处理,当我们想要这个值的时候,再调用IO函子中value这个函数
// map方法接收一个函数
map (fn) {
// map方法里面通过IO的构造函数创造了一个IO的函子
// 把当前的value和传入的fn组合成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
实例
const fp = require('lodash/fp')
// 创建一个IO的类
class IO {
// 创建一个静态的of方法,和之前不一样的是他要接收一个数据,返回一个IO函子
static of (value) {
return new IO(function () {
// 在这个函数里面,把刚刚of的值返回
return value
})
}
// 通过of方法可以感受到,IO函子最终想要的还是一个结果,只不过他把这个取值的过程包装到了一个函数里面来,进来我们需要这个值的时候,再来执行这个函数来取值
// 创建一个构造函数,传入一个fn函数
constructor (fn) {
// 把这个fn储存到value属性中来
this._value = fn
}
// 在map方法里面一样要传递一个fn
map (fn) {
// 在map方法里面一样要返回一个新的函子
// 我们要调用IO的构造函数,而不是of方法,因为在map方法里面,我们要把当前函子的这个value,也就是这个函数,和我们传入的函数组合成一个新的函数,而不是去调用函数处理值
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用
// 调用IO的of方法,返回一个新的函子
// 当我们调用of方法的时候,他会把我们取值的过程包装到一个函数里面,当我们需要的时候,才能获取这个process
// 调用map方法获取process中的某个属性,map需要一个函数,这个函数需要接收一个参数,这个参数就是of中传递的这个参数,参数传递之后就需要返回process中的某个属性
// execPath是当前load进程中的路径
let r = IO.of(process).map(p => p.execPath)
// console.log(r)
// => IO { _value: [Function (anonymous)] }
// 获取执行的结果
// console.log(r._value())
// /usr/local/bin/node
// ?结果好像有点不符合预期
因为_value和并了很多函数,所以他可能是不纯的函数,我们把这些不纯的操作延迟到调用的时候 — 通过IO函子将副作用控制在可控的范围内发生
6、Folktale
⑴、概念
- folktale一个标准的函数式编程库
- 和Lodash、ramda不同的是,他没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如: compose(函数组合)、curry(柯里化)等,一些函子Task、Either、MayBe等
⑵、安装folktale
$ npm i folktale
⑶、folktale 中的 compose、curry
// 导入folktale这个库
// 因为我们要对对象进行结构,所以我们直接解构出来,compose和curry这两个函数
// compose和curry这两个函数在在lambda模块中
const { compose, curry } = require('folktale/core/lambda')
// curry
// 定义一个变量接收柯里化之后的结果
// 求两个数的和
let f = curry(2, (x, y) => {
return x + y
})
console.log(f(1, 2))
// => 3
console.log(f(1)(2))
// => 3
// compose 函数组合,等同于Lodash中的flowRight
const { toUpper, first } = require('Lodash/fp')
// 通过结构的方式获取我们需要的函数,需要用到函数式编程,所以导入Lodash
// 定义一个函数,接收组合之后的函数
let v = compose(toUpper, first)
console.log(v(['one', 'two']))
// => ONE
7、Task函子
Task 异步执行: folktale(2.3.2)2.x中的Task和1.0中的Task区别很大,1.0中的用法更接近我们现在演示的函子, 这里以2.3.2来演示
// 读取文件(同目录文件里面的内容)
// 在node里面,使用的是fs读取模块
const fs = require('fs')
// 导入folktale,需要知道导入的内容在folktale的哪个模块中(folktale的官方文档)
// task在folktale2.0中,提供的是一个函数的形式,这个函数返回的是一个函子对象; 而在1.0中返回的是一个类
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
// 使用的模块准备好之后,写一个读取函数,传递一个文件路径(filename)
function readFile (filename) {
// 需要直接返回一个task函子,task函数本身就需要接收一个函数,这个函数是固定的(resolver)
// resolver里面有两个执行方法,执行成功/失败
return task(resolver => {
// readFile是异步来读取文件,需要三个参数:读取路径、编码、回调函数; 在node里面是错误优先,所以是先读取的err,然后是数据
fs.readFile(filename, 'utf-8', (err, data) => {
// 在这里面先是判断读取文件是否出现错误, 如果出现错误,那么就调用resolver的reject的方法,并且把错误传递进来
if (err) resolver.reject(err)
// 如果成功的话,直接调用resolver的resolve方法,并把接收到的数据传递进来
resolver.resolve(data)
})
})
}
// 调用
// 当我们调用readFile的时候,他会通过task这个函数给我们返回一个task函子
// 当前目录读取,所以直接是'package.Json'
readFile('package.json')
// map接收一个函数,这个函数是对数据进行处理(对每一行进行切割)的
.map(split('\n'))
// 寻找数组中具有 version的元素
.map(find(x => x.includes('version')))
// 当我们调用package.json的时候,他不会直接调用文件,而是返回一个task函子,当我们需要他读取文件的话,需要调用task函子中的run方法
.run()
// task函子中的listen方法,监听当前的执行状态
.listen({
// 以事件形式提供的,当我们失败的时候,执行的一个函数
onRejected: err => {
console.log(err)
},
// 当我们成功的时候,执行的函数
onResolved: value => {
console.log(value)
}
})
- package.json其实就是一行一行的数据,所以可以换行来进行切割,形成一个数组,再寻找数组中具有version这个元素; 所以需要用到Lodash中的split和find
- 在上导入Lodash中的split、find
- 在run之前调用map方法,map方法里面会处理拿到的这个返回结果,所以使用的时候就不用想这里面的机制了,我们之前是自己写函子,了解函子的内部机制,而在开发过程中就是直接来使用了
8、Point函子
- Pointed函子是实现了of静态方法的函子
- of方法是为了避免使用new来创建对象,更深层的涵义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)
class Container {
static of (value) {
// of方法是帮我们把值包裹到一个新的函子里面并且返回,这个返回的结果就是一个上下文
return new Container(value)
}
// 省略
}
Container.of(2)
.map(x => x + 5)
9、Monad 单子 (单细胞动物)
- monad函子是可以变扁的pointed函子 IO(IO(x))
- 一个函子如果具有of和join两个方法,并遵守一些定律就是monad函子
⑴、IO函子的问题
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))
}
}
// cat 读取文件内容,并将文件内容打印出来
// 实现原理:写一个读取文件函数,再写一个打印函数,再将他们组合起来
// 创建读取函数
let readFile = function(filename) {
// 因为读取文件的时候,会引起副作用,使我们的函数不纯,所以这一块我们不读取文件,而是返回一个IO的函子
return new IO(function() {
// 读取文件,需要导入fs模块(在上面引用fs)
// 处理的过程用同步开始做了,不再演示异步的过程
// 第一个参数是文件路径,第二个参数是文件编码
return fs.readFileSync(filename, 'utf-8')
})
}
// 第二个函数是打印
let print = function(x) {
return new IO(function() {
console.log(x)
return x
})
}
// 合并成cat函数
let cat = fp.flowRight(print, readFile)
// 调用
// 接收cat的返回结果
// 拿到的是一个嵌套式的函子,cat里面的形式是IO(IO(x))
let r = cat('package.json')
// => IO { _value: [Function (anonymous)] }
// cat返回的是函子,并没有读取文件,他把读取文件延迟执行了,当我们需要的时候再去调用这个函数
// ._value返回一个函子
let r = cat('package.json')._value()._value()
// => 读取了文件内容
console.log(r)
⑵、语法演示
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
})
}
- 首先,我们有个IO的类,和两个函数readFile和print,这两个函数都有一个共同的特点,都返回了一个IO函子
- 当我们读文件的时候(readFile函数),我们把读文件的操作封装成函数的话,那么他是一个不纯的操作,因为他要引用外部资源,会产生副作用
- 为了避免这个副作用,我们直接构造了一个函数,我们没有在这个函数里面读文件,而是返回了一个函子,我们把读文件的这个操作封装到了函子里面来,我们能保证当前的这个函数是纯的,因为根据输入,我们返回的是一个固定的输入,也就是一个IO函子、一个对象
- 调用的时候才会引发不纯的操作
//改造一下
const fp = require('lodash/fp')
const fs = require('fs')
// 将IO改造成monad方法 === IO Monad
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方法不需要任何参数,他会在这个方法里面调用这个value,并且把他的值返回
join () {
return this._value()
}
// 使用monad的时候,经常会把map和join联合起来使用; map的作用是把当前这个函数和我们的value组合起来,他返回一个新的函子,map在组合他们的时候,也会返回一个新的函子
// 所以我们调用join,是把他拍平,变扁
// flatmap的作用就是同事去调用map和join
flatMap (fn) {
// 在flatMap最终执行之后,我们去调用这个join,最终把join的结果,也就是这个函子返回
// return this.map(fn)
// 当我们调用map的时候,就会把value和fn合并,合并完成之后会返回一个新的函子(IO()),在这个函子中包裹的这个函数也会返回一个函子,所以我们要调用一下join
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(x => x.toUpperCase())
.map(fp.toUpper)
// 返回的是指用map,返回的是函子用flatMap
.flatMap(print)
// 只需要调用API实现相应功能,不需要了解内部的实现过程(可讲解)
// join就是在调用内部的value
.join()
// console.log(r)
// => 读取的内容
// 当我们想要编辑读取的内容(将读取的内容变成大写): 只需要在调用时候通过map方法来执行
console.log(r)
// => 读取的内容(大写)
十、要点总结(自测)
我与成长 、 至死方休 ~