为什么学习函数式编程?
函数式编程是一个非常古老的概念,它的出现甚至早于第一台计算机的诞生,函数式编程的历史可以通过《函数式编程的历史》这篇文章了解一下
为什么学习函数式编程?
- 函数式编程treact的流行受到了越来越多的关注react的高阶组件使用了高阶函数来实现,高阶函数就是函数式编程的一个特性
- vue3vue2做了很大的重构,更偏向于函数式编程
- 函数式编程可以抛this
- 使用函数式编程可以更好的利treesshacking来过滤无用的代码
- 使用函数式编程还可以方便测试,方便并行处理等
- 还有很多路可以帮助我们进行函数式编程的开发比lodash
函数式编程的概念
什么是函数式编程?
- 函数式编程英文名functioalPprogramme,简FP
- 函数式编程是一种编程的范式,可以认为是一种编程的风格,和面向对象是一种并列的关系
- 函数式编程我们可以认为是一种思维的模式加上它的实现方法,我们长听说的编程方式和面向过程编程和面向对象编程,面向过程编程,简单解释就是按照步骤来实现,一步一步来实现我们想要的功能,面向对象编程是把现实中的事物抽象成程序中的类和对象,然后通过封装、继承和多肽来演示事物之间的联系。
- 函数式编程的方式是把现实中的事物与事物之间的联系抽象到程序世界中
- 程序的本质是根据输入会得到相应的输出,程序开发过程中会涉及到很多输入输出的函数,函数式编程就是对这些运算过程抽象
- 函数式编程中的函数指的不是程序中的函数或者方法,函数式编程中的函数指的是数学中的函数,即映射关系,例如y=sin(x)中的x与y的关系
- 相同的输入始终得到相同的输出
- 函数式编程用来描述数据(函数)之间的映射
//非函数式
let num1 = 2
let num2 = 3
let sum = num 1 + num2
console.log(sum)
//函数式
function add(n1 , n2){
return n1 + n2
}
let sum = add(1,2)
console.log(sum)
函数是一等公民
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
在JavaScript中函数是一个普通的对象(可以通过new function()),我们可以吧函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new function(‘alert(1)’)来构造一个新的函数
把函数赋值给变量
let fn = function(){
console.log('hhh')
}
fn()
//示例
const obj = {
a(p){return vs.a(p)},
b(p){return vs.b(p)},
c(p){return vs.c(p)},
}
//优化
const obj = {
a: vs.a(p),
b: vs.b(p),
c:vs.c(p),
}
函数是一等公民是我们学习高级函数、柯里化等的基础
高级函数–函数作为参数
什么是高级函数?
-
高阶函数可以把函数作为参数传递给另外一个函数
-
可以把函数作为另外一个函数的返回结果
// 高阶函数-函数作为参数
// 封装一个forEach函数,遍历每一个数组中的元素
function forEach(array,fn){
for(let i = 0 ; i < array.length ; i++){
fn(array[i])
}
}
//测试一下这个函数
let arr = [1,3,5,7,9]
forEach(arr,function(item){
console.log(item)
})
// 封装一个fliter函数,过滤一个满足条件的数组
function filter (array,fn){
let result = []
for(let i = 0 ; i < array.length ; i++){
// 在fn中指定满足的条件,处理每个对象
// 如果满足条件
if(fn(array[i])){
result.push(array[i])
}
}
return result;
}
// 测试
let arr2 = [1,2,4,7,8]
let r = filter(arr2,function(item){
return item%2 === 0
})
console.log(r)//[ 2, 4, 8 ]
函数作为参数的好处?
- 函数作为参数可以让我们的参数变得更灵活,
- 而且我们在调用它的时候不需要考虑它是怎么实现的,
- 这个函数把内部实现的细节帮我们屏蔽了,
- 而且函数的名字是有实际意义的,
- 比如forEach这个名字就知道是遍历数据,filter是过滤数据
高级函数–函数作为返回值
// 高阶函数-函数作为返回值
// 想象一下,如果函数作为返回值的话,就是一个函数去生成另一个函数
// 下面是基本语法演示
function makeFn(){
let msg = 'hello function'
return function (){
console.log(msg)
}
}
// 调用方法1
const mf = makeFn();
mf();//hello function
// 调用方式2
makeFn()();//hello function
// ---------------------------
/**lodash中的once函数--对于一个函数只执行一次,
* 可以用于用户支付的时候,
* 不管用户点多少次按钮只支付一次订单
*/
function once (fn){
// 如何只执行一次呢?
// 1.定义一个变量,这个变量是一个标记
let done = false;//默认fn函数没有执行
return function(){//返回一个函数
if(!done){//2.先判断一下这个函数是否已经被执行过
// 如果判断fn函数没有执行过,
// 我们先把fn设置为执行过
done = true
// this指向当前的函数的 this
// arguments获取当前函数的参数
const _this = this;
console.log(_this,arguments)
return fn.apply(this,arguments)
}
}
}
// 通过once函数生成一个只能够执行一次的函数
let pay = once(function(money){
console.log(`支付:${money} RMB`)
})
pay(5)
pay(6)
pay(7)
pay(8)
高级函数的意义
- 抽象可以帮助我们屏蔽细节,只需要关注与我们的目标
- 高阶函数用来抽象通用问题
//面向对象的方式
let array = [1,2,3,4]
for(let i = 0 ; i < array.length ; i++){
console.log(array[i])
}
//高阶函数
let array = [1,2,3,4]
forEach(array,item=>{
console.log(item)
})
let r = filter(array,item=>{
return item % 2 === 0
})
常用的高阶函数
- forEach
- map
- filter
- every
- some
- find/findIndex
- reduce
- sort
- …
// 模拟常用高阶函数:map every some
// map--数组中的方法,对数组中的每一个成员遍历,并对每一个元素进行处理,然后把处理的结果存储到一个新的结果中返回
// 函数表达式的方式
const map = (array,fn) => {
let results = []
for(let value of array){
results.push(fn(value))
}
return results
}
// 测试
let arr = [1,2,3,4]
// 求每个数组值的平方的函数
arr = map(arr,v => v*v);
console.log(arr);//[ 1, 4, 9, 16 ]
/**
* map函数的好处是我们可以使用第二个参数来对数组中的每一个成员做任意处理,所以函数作为参数会让map参数更灵活
*/
//-------------------------------------
//every --判断数组中的每一个元素是否都匹配指定的条件
const every = (array,fn) =>{
// console.log(array,fn)
let result = true
for(let value of array){
result = fn(value)
if(!result){
break;
}
}
return result;
}
// 测试
let arr2 = [12,11,55]
let r2 = every(arr2,v =>v > 10);
console.log(r2)
/**
* every函数有一个参数时函数的时候也可以让我们的函数变得非常灵活,可以检测数组中的元素是否满足任意的条件
*/
//------------------------------------
// some--检测数组中的元素是否有一个满足指定的条件
const some = (array,fn) =>{
// console.log(array,fn)
let result = true
for(let value of array){
result = fn(value)
if(result){
break;
}
}
return result;
}
// 测试
// 检测数组中是否有偶数
let arr3 = [1,2,3,9]
let r3 = some(arr3,v => v%2 === 0)
console.log(r3)
// 高阶函数:通过把一个函数传递给另一个函数可以让这个函数更灵活
闭包
闭包的概念:
- 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包.
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数作用域中的成员
//函数作为返回值
function makeFn(){
let msg = 'hello function'
return function (){
console.log(msg)
}
}
const fn = makeFn()
fn()
//once
function once (fn){
let done = false;
return function(){
if(!done){
done = true
return fn.apply(this,arguments)
}
}
}
let pay = once(function(money){
console.log(`支付:${money}RMB`)
})
pay(5)
pay(5)
闭包的本质
函数在执行的时候回放在一个执行栈上,当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以反问外部函数的成员.
闭包–案例
案例1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>closure</title>
<script>
// Math.pow(2,2)
// Math.pow(3,2)
// 封装幂函数
function makePower(power){
return function(number){
return Math.pow(number,power)
}
}
let power2 = makePower(2);//求2次幂
let power3 = makePower(3);//求3次幂
console.log(power2(4))
console.log(power2(5))
console.log(power3(4))
</script>
</head>
<body>
</body>
</html>
案例2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>closure</title>
<script>
// 求取不同级别员工的工资= 绩效工资+ 基础工资
function makeSalary(base){
return function(performance){
return base + performance
}
}
let sl1 = makeSalary(12000);//级别1的基本工资
let sl2 = makeSalary(15000);//级别2的基本工资
console.log(sl1(3000));//级别1的绩效工资
console.log(sl2(3000));//级别2的绩效工资
// 在浏览器的sources打断点调试,看Call Stack(调用栈)和Scope(作用域),观察Scope查看闭包是什么时候发生的
</script>
</head>
<body>
</body>
</html>
纯函数
纯函数的概念
- 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- 纯函数类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
- lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等的操作的一些方法
- 数组的slice和splice分别是纯函数和不纯的函数
- slice返回的是数组中指定部分,不会改变原来的数组
- splice 对数组进行操作返回该数组,会改变原数组
// 纯函数和不纯的函数
//案例 slice / splice
let arr = [1,2,3,4,5]
// 纯函数:相同的输入有相同的输出
console.log(arr.slice(0,3))
console.log(arr.slice(0,3))
console.log(arr.slice(0,3))
// 不纯的函数:相同的输入有不同的输出
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
console.log(arr.splice(0,3))
// -------------------------
// 自己写一个纯函数
function getSum (n1 , n2){
return n1 + n2
}
console.log(getSum(1,3))
console.log(getSum(1,3))
console.log(getSum(1,3))
// 函数式编程不会保留计算中间的结果,所以变量是不可变得(无状态的)
//当我们在调用这个函数的时候,我们吧一个函数的执行结果交给另一个函数去处理
lodash(纯函数的代表)
Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。
// 演示lodash的几个常用方法
// first / last / toUpper / reverse / each / includes / find / findIndex
// 配置package.json文件 npm init -yes
// 安装lodash npm i lodash
const _ = require('lodash');
let arr = ['yrn','hcb','pjg','slg'];
console.log(_.first(arr));//@return — Returns the first element of array.获取第一个
console.log(_.last(arr))//@return — Returns the last element of array.获取最后一个
console.log(_.toUpper(arr[0]))//@return — Returns the upper cased string.全部大写
console.log(_.reverse(arr));//翻转数组
// 遍历简写
_.each(arr,(item,index)=>{
console.log(item,index)
})
// includes:判断在当前集合中是否存在这个值,索引默认从0开始,如果是负数就从末尾开始检索
console.log(_.includes(arr,'hcb',0))
//find 在集合中查找到第一个匹配的元素并返回,第二个参数可以是Array|Function|Object|string
const users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false },
{ 'user': 'pebbles', 'age': 1, 'active': true }
];
console.log(_.find(users,['active',false]));//{ user: 'fred', age: 40, active: false }
console.log(_.find(users,o => o.age < 36))
console.log(_.find(users,'active'))
// findIndex类似find区别是返回的是第一个查找到的元素的索引,而不是元素本身
console.log(_.findIndex(users,'active'))
纯函数的优势
- 可缓存----提高程序的性能
- 因为相同的输入始终会有相同的输入,所以可以把纯函数的结果缓存起来
- 场景:当函数需要多次调用的时候我们可以缓存起来用来提高性能
//记忆函数 memoise函数
const _ = require('lodash');
// 计算圆的面积
function getArea(r){
console.log(r)
return Math.PI * r * r
}
// 调用memoise使getArea方法的返回值有缓存
// let getAreaWithMemory = _.memoize(getArea);
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
/**4
* 50.26548245743669
* 50.26548245743669
* 50.26548245743669
* 50.26548245743669
* 从输出结果我们可以看到getArea函数只调用了一次
*/
//-------------------------------------
// memoise函数的内部实现过程
function memoize(f){
let cache = {}
return function(){
let key = JSON.stringify(arguments)
// 先判断返回中是否有值,如果有值直接返回,如果没有值就返回f函数
cache[key] = cache[key]|| f.apply(f,arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 测试的结果和刚才的一样,说明我们模拟的结果是ok的
- 可测试
- 并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要范文共享的内存数据,所以在并行环境下可以任意运行纯函数,如JavaScript现在多出了Web Worker线程
函数的副作用
副作用会让一个函数变得比纯,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证相同的输出,就会带来副作用
副作用的来源
- 配置文件
- 数据库
- 获取用户的输入
- 。。。。。。。。。。
所有的外部交互都有可能带来副作用,副作用也使得方法的通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是不作用不可能完全禁止,尽可能控制它们在可控范围内发生
柯里化(curry)
// 柯里化演示
// function checkAge(age){
// let min = 18
// return age >= min
// }
//--------------------------
// 进一步改进---普通的纯函数
// function checkAge (min,age){
// return age >= min
// }
// console.log(checkAge(18,20))
// console.log(checkAge(18,24))
// console.log(checkAge(19,22))
//-----------------------------
// 当最小值一直都是18的时候上面这种调用方法就不合适了
// 更进一步改进----闭包/高级函数
// function checkAge(min){
// return function(age){
// return age >= min
// }
// }
// es6写法
let checkAge = min => (age => age >= min);
let checkAge18 = checkAge(18);//最小年龄18
let checkAge20 = checkAge(20);//最小年龄20
console.log(checkAge18(20))
console.log(checkAge20(24))
/**
* 以上这种形式就是函数的柯里化
* 当我们的函数有多个参数的时候,我们可以对这个函数进行改造
* 我们可以调用一个函数只传递部分的参数,
* 并且让这个函数返回新的函数,
* 让这个新的函数去接收剩余的参数并且返回相应的结果,
* 这就是函数的柯里化
* */
/**
* 总结:
* 柯里化(currying)
* 1.当一函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
* 2.然后返回一个新的函数接收剩余的参数,返回结果
*/
lodash中的柯里化函数
- _.curry(func)
- 功能:创建一个函数,该函数接收一个或多个func的函数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接收剩余的参数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
// lodash中的curry基本使用
const _ = require('lodash')
//有三个参数叫三元函数,有几个参数就叫几元函数
function getSum(a,b,c){
return a + b + c;
}
// 柯里化可以把多元函数最终转化为一个一元函数
const curried = _.curry(getSum)
console.log(curried(1,2,3));//仅仅是包装没意义
// 如果我们传递getSum的部分参数,
// curry会返回一个新的函数用来接收剩余参数
console.log(curried(1)(2,3))//此时结果是一样的
// lodash的curry函数是吧多元函数编程一元函数
console.log(curried(1,2)(3))
柯里化案例
// 柯里化案例
// 假设我们判断字符串中是否有空白字符或提取字符串中的空白字符可以使用字符串的match方法
// 面向对象方法
// ''.match(/\s+/g);//匹配提取字符串中的所有空白字符
// ''.match(/\d+/g);//匹配提取字符串中的所有数字
// 那如何提取数组中的元素的空白字符或数字呢?
// 使用函数式的方法匹配提取字符串中的内容
// match纯函数
// function match(reg,str){
// return str.match(reg)
// }
const _ = require('lodash')
// 进行柯里化处理
const match = _.curry(function(reg,str){
return str.match(reg)
})
// 判断是否有空白字符
const haveSpace = match(/\s+/g)
console.log(haveSpace('helloworld'));//如果有空白字符就会以数组的形式返回提取出来的空白字符,如果没有空白字符就会返回null
// 判断是否有数字
const haveNumber = match(/\d+/g)
console.log(haveNumber('hcb5d6'))
// 现在我们要过滤一个数组,要找到这个数组中所有具有空白字符的元素
const filter = _.curry(function(func,array){
return array.filter(func)
})
console.log(filter(haveSpace,['john Connor','john_Donne']));//'john Connor'
// 但是我们直接定义filter这样的意义并不大,我们还可以改造一下
//我们可以利用filter来生成这种具有特定功能的函数
const findSpace = filter(haveSpace);
console.log(findSpace(['john Connor','john_Donne']))
// 需要注意的是,函数式编程能够让我们定义一次然后可以重复性的使用
柯里化原理模拟
// 柯里化的实现原理
// 模拟实现lodash中的curry方法
// const _ = require('lodash')
function getSum(a,b,c){
return a + b + c;
}
// const curried = _.curry(getSum)
// console.log(curried(1,2,3));
// console.log(curried(1)(2,3))
// console.log(curried(1,2)(3))
// curry函数内部实现
function curry (func){
// 当传入一个普通纯函数时会返回一个新的函数
console.log(func.length);
return function curriedFn (...args){//标记:回调函数1
// 判断当前函数获取到的实参的个数是否小于func的形参的个数
if(args.length < func.length ){
// 当是参个数小于形参个数时,
//表示curry函数只传递了一部分参数
// 此时返回一个新的函数
return function(){//标记:回调函数2
// 在这个函数中应该再次执行一些回调函数1
// 这里我们用的是递归的方式执行curriedFn
// 此时传递的参数应该是回调函数1的参数与回调函数2的合并值
// 因为这个函数的参数arguments是一个伪数组,需要转换成数组
return curriedFn(...args.concat(Array.from(arguments)))
}
}
// 当实参个数大于等于形参个数时
// 此时相当于一次性传递全部参数
return func(...args);
}
}
const curried = curry(getSum)
console.log(curried(1,2,3));
console.log(curried(1)(2,3))
console.log(curried(1,2)(3))
柯里化总结:
- 柯里化可以让我们给函数传递较少的参数, 从而得到一个已经记住某些固定参数的新函数
- 这是一种使用闭包对函数参数的缓存
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
函数组合(compose)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdQ0oKS9-1597675856476)(3A7C0AA4395043C6A412443CA1866115)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPrpDAWj-1597675856478)(73DF4DC0A51E4989B47730FE90EA42D8)]
// 函数组合(compose)
// 函数组合演示
// 传入两个函数f和g
// 注意函数组合 组合的是纯函数,从右往左执行函数
function compose(f,g){
return function(value){
return f(g(value))
}
}
function reverse(array){
return array.reverse()
}
function first (array){
return array[0]
}
const last = compose(first,reverse)
console.log(last([1,2,3,4]))
lodash中的组合函数
- lodash中的组合函数flow()和flowRight(),他们都可以组合多个函数
- flow()从左到右执行
- flowRight()从右到左执行
/**
* lodash中的函数组合方法flow和flowRight
* flow()从左到右执行,flowRight()从右到左执行
* 下面演示_.flowRight()方法实现获取数组中的最后一个元素并转换成大写
*/
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const getLastValue = _.flowRight(toUpper,first,reverse)
console.log(getLastValue(['hcb','pgl','hzx']))
组合函数的实现原理
// 函数组合的使用原理
// 模拟lodash中的flowRight方法的函数组合实现原理
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const getLastValue = _.flowRight(toUpper,first,reverse)
console.log(getLastValue(['hcb','pgl','hzx']))
// reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
// 具体看https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
// 模拟compose函数
// 1.传递的参数为函数
// function compose(...args){
// // 2.用剩余参数...获取函数类参数的数组
// // 3.返回一个函数,这个返回的函数需要做预处理,这个function中需要接受一个参数value
// return function(value){
// //4.因为函数组合中执行的函数是从右到左执行的,我们需要把函数类参数的数使用reverse翻转一下
// // 5.然后使用reduce函数执行该数组,把每个数组中的元素去执行一个我们需要的函数,最后汇总成一个结果返回
// return args.reverse().reduce(function(acc,fn){//参数1是上一次调用回调返回的积累结果或者初始结果,参数二是数组中正在处理的元素,在这个函数组成的数组中每个元素都是函数
// return fn(acc)
// },value)
// }
// }
// es6简写
const compose = (...args)=>value=>args.reverse().reduce((acc,fn)=>fn(acc),value);
const glv = compose(toUpper,first,reverse);
console.log(glv(['hcb','pgl','hzx']))
函数组合–结合律
函数组合要满足结合律,这个结合律是数学中的结合律,假设我们把三个函数组合成一个函数,那么我们可以把前两个函数组合,也可以把后两个函数组合,结果都是一样的
// 函数组合要满足结合律
const _ = require('lodash')
// const reverse = arr => arr.reverse()
// const first = arr => arr[0]
// const toUpper = s => s.toUpperCase()
// 使用lodash中的纯函数,简单方便
// const getLastValue = _.flowRight(_.toUpper,_.first,_.reverse)
// console.log(getLastValue(['hcb','pgl','hzx']))
// 下面演示结合律,当我们组合函数的时候可以先组合前两个函数也可以组合后两个函数,都是一样的
// const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse))
console.log(f(['hcb','pgl','hzx']))
函数组合–调试
// 函数组合如何调试
// 案例把字符串NEVER SAY DIE 转换成 never-say-die
const _ = require('lodash');
// 检测组合函数中的函数
// const log = (v)=>{
// console.log(v);
// return v
// }
// 进一步确定追踪的对象
const trace = _.curry((tag,v)=>{
console.log(`${tag}`,v)
return v
})
// 去空格分割字符串转数组
const split = _.curry((spe,str)=>_.split(str,spe))
// 大写转成小写
// _.toLower
// toLower会把数组直接用逗号拼接成字符串,我们需要用map函数处理一下
const map = _.curry((fn,array)=> _.map(array,fn))
// 数组转成字符串
const join = _.curry((spe,arr)=>_.join(arr,spe))
// 函数组合
const f = _.flowRight(join('-'),map(_.toLower),trace('split之后'),split(' '))
console.log(f('NEVER SAY DIE'))
lodash中的fp模块
// lodash中的fp模块,对函数是变成提供了友好的支持,fp模块提供的每一个方法都是柯里化的,如果有多个参数的话都是函数优先数据滞后,这些方法都可以在函数组合的时候使用
// lodash处理
const _ = require('lodash');
// 检测组合函数中的函数
// const log = (v)=>{
// console.log(v);
// return v
// }
// 进一步确定追踪的对象
const trace = _.curry((tag,v)=>{
console.log(`${tag}`,v)
return v
})
// 去空格分割字符串转数组
const split = _.curry((spe,str)=>_.split(str,spe))
// 大写转成小写
// _.toLower
// toLower会把数组直接用逗号拼接成字符串,我们需要用map函数处理一下
const map = _.curry((fn,array)=> _.map(array,fn))
// 数组转成字符串
const join = _.curry((spe,arr)=>_.join(arr,spe))
// 函数组合
const f = _.flowRight(join('-'),map(_.toLower),trace('split之后'),split(' '))
// console.log(f('NEVER SAY DIE'))
//------------------------------------------------------------------------------
// fp模块处理
// fp模块简化,减少了上面各种方法的包装
const fp = require('lodash/fp');
const f2 = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
console.log(f2('NEVER SAY DIE'))
lodash-map方法的小问题
// lodash 和lodash/fp 模块中 map 方法的区别
//区别:所接收的函数的的参数不一样,lodash中map的函数所接收参数是三个,fp中的map所接收的参数是一个
// 调用map方法把数组中的所有元素都转换成整数
// lodash的处理方式
const _ = require('lodash')
console.log(_.map(['23','8','10'],parseInt));//[ 23, NaN, 2 ]
// 为什么会打印出NaN呢?
// 鼠标放在map上我们可以看到该方法的解释中有这么一段话Creates an array of values by running each element in collection through iteratee. The iteratee is invoked with three arguments: (value, index|key, collection).
// map的第二个参数是一个函数,函数必须有三个参数:值,索引/键,集合
// 下面我们展开一下map调用perseInt方法的过程
_.map(['23','8','10'],function(value,index,array){
// console.log(value,index,array)
// parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于2-36之间的数。
// perseInt的两个参数分别是string(被解析的字符串),radix(几进制)
// 按顺序传递参数给parseInt会造成value对标string,index对标radix
return parseInt(value,index,array)
})
// 这样造成的结果就是
// parseInt('23',0,array)
// parseInt('8',1,array)//8为1进制,不存在的,进制的范围是2-36,所以返回的是NaN
// parseInt('10',2,array)
// 解决上面的问题可以自己封装一个parseInt,只接受一个参数,这样就可以解决了
//------------------------------------------
// fp模块的map方法就不会出现这样的问题
const fp = require('lodash/fp')
// fp模块的map方法只传递一个参数的时候接收的是一个函数,因为fp模块中的方法是函数优先的,当前这甘薯只接收一个参数,而lodash中的函数所接收的参数是三个
console.log(fp.map(parseInt,['23','8','10']))
Point Free
// point free
/**
* Point Free 概念:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数.
*
* 重点关注下面三句话:
* 1.不需要指明处理的结果
* 2.只需要合成运算过程
* 3.需要定义一些辅助的基本运算函数
*
*/
// hello World => hello_world
// 两个步骤:
// 1.转换成小写
// 2.把空格替换成_
const fp = require('lodash/fp')
const aToB = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)
console.log(aToB('hello World'))
Point Free案例
// point free 案例
//把一个字符串中的首字母提取并转换成大写,使用.号分隔符
// world wild web ==> W.W.W
const fp = require('lodash/fp')
// const aToB = fp.flowRight(fp.join(', '),fp.map(fp.first),fp.map(fp.toLower),fp.split(' '))
// 上面我们调用了两次map,如何能值调取一个map?看下面
const aToB = fp.flowRight(fp.join(', '),fp.map(fp.flowRight(fp.first,fp.toLower)),fp.split(' '))
console.log(aToB('world wild web'))
functor(函子)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AsotNekp-1597675856478)(5DADBE7FDAB24C4E87973F51FEAE1E88)]
//functor 函子,把副作用控制在可控范围内, 函子可以处理异常和异步操作
// 函子是一个特殊的容器,通过一个普通的对象来实现,该对象 具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
// 函子是一个对象,并且维护一个值,对外公布map方法
// class Container {
// constructor(value){
// // 现在函子这个盒子里要维护一个值,这个值不对外公布
// this._value = value
// }
// // 现在我们对外公布map方法,接收一个处理值得函数
// // 这个函数是纯函数,因为我们要把value传递给这个函数,由这个函数来处理这个值
// map(fn){
// // 当我们调用map方法的时候,map方法会调用fn处理这个值,并且把处理的结果返给一个新的函子,由新的函子来保存
// // 返回一个新的值的时候,我们把fn函数处理的值传递给Container
// return new Container(fn(this._value))
// }
// }
// let r = new Container(5)
// .map(x=>x + 1)
// .map(x => x * x)
// console.log(r)
// //所以我们的map方法返回的不是值而是一个新的函子对象,在这个新的函子对象里面保存一个新的值,我们始终不把值对外公布,我们想要处理值的话就要给map对象传递一个处理值得函数
//----------------------如何不使用new实例化Container呢?
// 在Container中创建静态方法,返回实例化 的Container
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)
// ---------------------------
// 如果 给函子传递null undefined就会报错 ,怎么解决这类问题?
Container.of(null)
.map(v=>v.toUpperCase())
函子总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpHB7iM8-1597675856479)(28F9800C8F3B4269B044A447AD3D22D9)]
Maybe函子(处理空值的问题)
- 我们在编程过程中可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
// MayBe 函子 处理空值的问题
class MayBe {
constructor (value){
this._value = value
}
static of(value){
return new MayBe(value)
}
map(fn){
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
// 判断传入的值是不是null或undefined
isNothing(){
return this._value === null || this._value === undefined
}
}
const m = MayBe.of('hello world')
.map(v=> v.toUpperCase())
// console.log(m);//MayBe { _value: 'HELLO WORLD' }
const b = MayBe.of(null).map(v => v.toUpperCase())
// console.log(b)//MayBe { _value: 'null' }
// -----------------------------------------
// maybe函子的问题:当我们多次调用map的时候我们不知道哪一次传入了空值
const c = MayBe.of('hello world')
.map(v=> v.toUpperCase())
.map(v=> null)
.map(v=> v.split(' '))
console.log(c)
Either函子
- Either两者中的任何一个,类似if…else…的处理
- 异常会让函数变得不纯,Either函子可以用来处理异常
// Either函子,处理异常问题并且可以记录下来出错的信息
class Left {//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 Left.of(fn(this._value))
}
}
function parseJson (value){
try {
return Right.of(JSON.parse(value))
} catch (error) {
return Left.of({error:error.message})
}
}
// let r = parseJson('{hcb:hhh}')
let r = parseJson('{"hcb":"hhh"}')
.map(x => x.hcb.toUpperCase())
console.log(r)
IO函子
- IO函子中的_value是一个函数,这是把函数当做值来处理
- IO函子可以把不纯的动作存储到_value中,延时执行这个不纯的操作(惰性操作),包装当前纯的操作
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class IO {
static of(value){
return new IO(function(){
return value
})
}
constructor(fn){
this._value = fn
}
map(fn){
// 这里使用new IO的构造函数而不是使用IO.of方法是因为在map方法里面我们需要把当前函子的value也就是这个函数和我们传入的这个函数组合成一个新的函数,而不是去调用函数处理值,这是跟以前不一样的地方
return new IO(fp.flowRight(fn,this._value))
}
}
const r = IO.of(process)//当我吗调用of方法时,它会把我们传入的值包装在一个函数里来,当我们需要的时候再来获取这个值
.map(p=>p.execPath)
console.log(r);//IO { _value: [Function] }
//我们当前调用完这个函数之后会返回一个IO函子,这个IO函子的value保存的是一个function,我们可以直接调用一下
console.log(r._value())//:\Program Files\nodejs\node.exe当前进程的路径
folktale
Task异步执行
- 异步任务的实现过于复杂,我们使用folktale中的task来演示
- folktale是一个标准的函数式编程库
- 和lodash、ramda不同的是,他没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如compose、curry等,一些函子Task、Either、MayBe等
// folktale是一个 函数式编程库,其中的 task方法可以处理异步操作的问题
// 下面 我们来看一下folktale中的compose和curry
const { compose , curry } = require('folktale/core/lambda')
const { toUpper , first } = require('lodash/fp')
// folktale中的curry和lodash 中的curry还是有些区别的
// 柯里化curry
// curry的第一个参数 声明传递的参数个数,第二个参数是传入的函数
// let f = curry(2,(x,y)=>{
// return x + y
// })
// console.log(f(1,2))
// console.log(f(1)(2))
// -------------------------------------
// compose(函数组合)
let f = compose(toUpper,first)
console.log(f(['ind','hcb']))
Task函子
Task异步执行
- folktale(2.3.2)2.x中的Task和1.0中的Task区别喊打,1.0中的用法更接近我们现在演示的函子
- 这里用2.3.2来演示
// task 处理异步任务
// 案例通过读取文件演示异步任务
// 读取package.json文件,并把version解析出来
// task在folktale^2.0版本提供的是函数 形式,在folktale^1.0版本提供的是类
const { task } = require('folktale/concurrency/task')
// node环境读取文件使用fs模块
const fs = require('fs');
//切割 package.json并找到version需要用到lodash/fp模块的split和find
const { split , find } = require('lodash/fp')
// 写一个读取文件的函数
function readFile(filename){//传入文件的路径,同层相对路径可以直接写名字
// 返回一个task函子,task函数的返回值是一个Task对象,
// task函数本身需要接收一个函数,这个函数的参数时固定的叫resolver
return task(resolver =>{
// resolver是一个对象,提供了两个方法
// resolve执行成功调用的方法
// reject执行失败调用的方法
//fs.readFile是异步读取文件
fs.readFile(filename,'utf-8',(err,data) =>{
// 先判断读文件的时候是否出错了,如果出错调用resolver.reject方法
if(err) resolver.reject(err)
// 如果执行成功调用resolver.resolve方法
resolver.resolve(data)
})
})
}
// 执行读取文件的函数,当我们读取文件的时候返回的是一个task函子
// readFile('package.json')
// // 想要读取文件的话调用Task函子中的run方法
// .run()
// // Task函子还提供了监听事件方法listen(),用来接收resolve和reject的值
// .listen({
// onRejected : err =>{
// console.log(err)
// },
// onResolved: value=>{
// console.log(value)
// }
// })
// 执行node命令可以看到读出来的文件
//------------------------------------------------
//我们知道readFile方法返回的是一个Task函子,而Task函子都有一个map方法
//所以在run直接我们可以调用Task函子的map方法,在map方法里面来处理拿到的结果
readFile('package.json')
// 切割换行符,截取字符串返回数组
.map(split('\n'))
// 通过find方法查找数组中的每一项是否具有version
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected : err =>{
console.log(err)
},
onResolved: value=>{
console.log(value)
}
})
point函子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ev7UY02w-1597675856480)(486F4B49DE92416DA9AB612C6E94DD09)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fngtTds9-1597675856481)(80630BFB09DC4609B01C2D237D56009A)]
IO函子的问题
/**
* IO 函子 的问题
* IO函子回顾 :
* IO函子中的_value是一个函数,这是把函数当做值来处理
* IO函子可以把不纯的动作存储到_value中,延时执行这个不纯的操作(惰性操作),包装当前纯的操作
* 把不纯的操作交给调用者来处理
*
*/
const fs = require('fs');
const fp = require('lodash/fp')
class IO {
static of(value){
return new IO(function(){
return value
})
}
constructor(fn){
this._value = fn
}
map(fn){
// 这里使用new IO的构造函数而不是使用IO.of方法是因为在map方法里面我们需要把当前函子的value也就是这个函数和我们传入的这个函数组合成一个新的函数,而不是去调用函数处理值,这是跟以前不一样的地方
return new IO(fp.flowRight(fn,this._value))
}
}
// const r = IO.of(process)//当我吗调用of方法时,它会把我们传入的值包装在一个函数里来,当我们需要的时候再来获取这个值
// .map(p=>p.execPath)
// console.log(r);//IO { _value: [Function] }
// //我们当前调用完这个函数之后会返回一个IO函子,这个IO函子的value保存的是一个function,我们可以直接调用一下
// console.log(r._value())//:\Program Files\nodejs\node.exe当前进程的路径
// // ----------------------------------------
// linux下有一个cat命令,这个命令的作用是读取文件的内容,
// 并且把内容打印出来,下面模拟一下这个命令
// 先写一个读取文件的函数,再写一个打印的函数,然后组合成一个cat函数
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))//返回的是 嵌套函子
let r = cat('package.json')._value()._value()
console.log(r)
/**
* IO函子的问题:
* 我们在调用嵌套函子的时候非常不方便,如果函子有嵌套的话,
* 我们想要调用嵌套函子中的函数,我们需要._value()._value(),
* 虽然这样也可以实现,但是这种api的风格看起来很不爽
*/
Monad函子
- Monad函子是可以变扁的Ponit函子,可以解决函子嵌套的问题,
- 一个函子如果具备join和of两个方法并遵守一定的 定律就是Monad
/**
* Monad解决 可以IO 函子 的问题
* Monad函子是可以变扁的Ponit函子,可以解决函子嵌套的问题,
* 一个函子如果具备join和of两个方法并遵守一定的 定律就是Monad
* IO函子的问题:
*
* IO函子的问题:
* 我们在调用嵌套函子的时候非常不方便,如果函子有嵌套的话,
* 我们想要调用嵌套函子中的函数,我们需要._value()._value(),
* 虽然这样也可以实现,但是这种api的风格看起来很不爽
*/
const fs = require('fs');
const fp = require('lodash/fp')
class IO {
static of(value){
return new IO(function(){
return value
})
}
constructor(fn){
this._value = fn
}
map(fn){
// 这里使用new IO的构造函数而不是使用IO.of方法是因为在map方法里面我们需要把当前函子的value也就是这个函数和我们传入的这个函数组合成一个新的函数,而不是去调用函数处理值,这是跟以前不一样的地方
return new IO(fp.flowRight(fn,this._value))
}
join () {
return this._value()
}
flatMap(fn){
// 该方法的作用就是同时调用map和join
return this.map(fn).join()
}
}
// linux下有一个cat命令,这个命令的作用是读取文件的内容,
// 并且把内容打印出来,下面模拟一下这个命令
// 先写一个读取文件的函数,再写一个打印的函数,然后组合成一个cat函数
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())
.flatMap(print)
.join()
console.log(r)
/**
* 什么是monad?
* 就是一个具有静态的IO方法并且具有专业方法的这么一个函子
* 什么时候使用monad呢?
* 让一个函数返回一个函子的时候,我们要想到monad
* monad可以帮我们解决函子嵌套的问题
* 当我们想要合并一个函数,并且这个函数返回一个值,
* 这个时候我们可以调用map方法
* 当我们想要去合并函数,但是这个 函数返回一个函子,
* 这个时候我们要用flatMap方法
*/