01JS基础_面向函数式编程

一、函数式编程概念

内容思维图总览

1、为什么要学习函数式编程

  1. 函数式编程随着react的流行受到越来越多的关注
  2. Vue3也开始拥抱函数式编程
  3. 函数式编程可以抛弃this
  4. 打包过程中可以更好的利用tree shaking过滤无用代码
  5. 方便测试, 方便并行处理
  6. 有很多库可以帮助我们继续函数式开发:lodash、underscore、ramda


2、什么是函数式编程

函数式编程(FB),是编程范式之一, 我们常听说的编程还有面向过程编程、面向对象编程

思维方式: 把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

  1. 程序的本质: 根据输入通过某种运算获得相应的输出, 程序开发过程中会设计很多有输入和输出的函数
  2. x > f(联系、映射) > y, y = f(x)
  3. 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如: y = sin(x), x和y的关系
  4. 相同的输入始终要得到相同的输出(纯函数)
  5. 函数式编程用来描述数据(函数)之间的映射(其实就是对运算过程的重现)

非函数式(面向过程)

//先定义两个数
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、使用高阶函数的意义

  1. 函数是编程的核心思想, 需要对运算过程进行抽象, 然后可以再很多地方去重复用这个函数
  2. 抽象可以帮我们屏蔽细节, 只需要关注我们的目标
  3. 高阶函数是用来抽象通用的问题

循环打印一个数组中的数组元素

//面向过程的方式
// 定义一个数组
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就不会被释放
  1. 在一个作用域中,可以调用这个函数的内部函数
  2. 当调用函数时,可以访问到函数作用域的内部成员
  3. 闭包的作用就是把内部函数的作用范围延长了


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、闭包案例

  1. 求函数平方: 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、纯函数的好处

⑴、好处一

  1. 可缓存: 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
  2. 实例:有一个函数非常常用,每一次使用都需要等段时间才能得到结果,这对性能来说不好的
  3. 解决方案: 当这个函数第一次执行的时候,把这个结果缓存起来,当我们第二次调用的时候,直接从缓存中得到结果,从而提高性能

记忆函数: 牺牲内存,优化性能

// 引用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

⑵、好处二

可测试: 纯函数让测试更方便


⑶、好处三

并行处理:

  1. 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
  2. 纯函数不需要访问共享的内存数据,所以再并行环境下可以任意运行纯函数(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
}
  1. 副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
  2. 副作用来源: 全局变量(如上例)、配置文件、数据库、获取用户的输入…

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降,不合适扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生




六、Lodash

  1. lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
  2. 现代化实用的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中的组合函数

  1. Lodash中组合函数flow()或者flowRight(),他们都可以组合多个函数
  2. flow()是从左到右运行
  3. 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. 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
  2. 然后返回一个新的函数接收剩余的参数,返回结果

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. 柯里化可以让我们给一个函数传递较少的参数, 得到一个已经记住了某些固定参数的新函数
  2. 这是一种对函数参数的’缓存’(闭包)
  3. 让函数更灵活,让函数粒度更小(因为柯里化可以生成粒度更小的函数),目的是为了之后组合的时候使用
  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

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前,我们需要定义一些辅助的基本运算函数 — 组合函数


⑴、概念

  1. 不需要指明处理的数据
  2. 只需要合成运算过程
  3. 需要定义一些辅助的基本运算函数

⑵、案例一: 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)
// => 读取的内容(大写)



十、要点总结(自测)

在这里插入图片描述






我与成长 、 至死方休 ~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后海 0_o

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值