大前端 - 函数式编程范式

函数式编程范式

为什么要学习函数式编程

函数式编程是一个非常古老的概念,早于第一台计算机的诞生。
那我们为什么要学函数式编程 ?

  • 函数式编程时随着React的流行受到越来越多的关注
  • Vue3 也开始拥抱函数式编程
  • 函数式编程可以抛弃this
  • 打包的过程中可以更好的利用tree shaking过滤无用代码
  • 方便测试,方便进行处理
  • 有很多库可以帮助我们进行函数式开发:lodash,underscore,ramda

什么是函数式编程

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

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物间的联系
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
    • x -> f(联系、映射) -> y, y = f(x)
    • 函数式编程中的函数不是指程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程用来描述数据(函数)之间的映射
// 非函数式
let num1 = 10
let num2 = 20
let sum = num1 + num2


// 函数式
function add(n1, n2) {
	return n1 + n2
}

let sum = add(10, 20)

总结:函数式编程,一定会有输入和输出,好处是可以使代码重复利用

函数是一等公民

为什么函数是一等公民 ?

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数可以作为返回值

在编程的语言中,一等公民可以做为函数参数,可以作为 函数的返回值,也可以赋值给变量。
在JavaScript中函数就是一个普通的对象(可以通过 new Function( )),我们可以把函数存储在变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function(alert(‘1’)) 来构造一个新的函数.

// 把函数赋值给变量
let fn = function () {
	console.log('hello function')
}

// 一个示例
const BlogController {
  	index(posts) { return Views.index(posts) },
  	show(post) { return Views.show(post) },
  	create(attr) { return Db.create(attr) },
  	update(post,attrs) { return Db.update(post,attrs) },
  	destory(post) { return Db.destory(post) }
}

// 优化
const BlogController {
	index: Views.index,
  	show: Views.show,
  	create: Db.create,
  	update: Db.update,
  	destory: Db.destory
}

谨记:函数是一等公民是学习高阶函数、柯里化的基础!!!!

高阶函数

什么是高阶函数 ?
  • 可以把函数作为参数传递给另一个参数
  • 可以把函数作为另一个函数的返回结果
// 高阶函数 - 函数作为参数

/**
 * 例子: 封装一个forEach方法
 * 分析: 当遍历整个数组时,对这个数组每一项做处理,但是可能处理是不同的,
 * 比如可能打印数组的每一项,或者是把数组的每一项赋值给某个元素,因此当需要
 * 传入变化的内容,可以使用回调函数
 */

function forEach(array, fn) {
    for(let i = 0; i < array.length; i++) {
        // 调用传入的函数 并把数组的每项赋值
        fn(array[i])
    }
}

let arr1 = [1, 2, 3, 4]
// 打印每个元素
forEach(arr1, function(item) {
    console.log(item)
})

// 让每个元素 + 1
forEach(arr1, function(item) {
    item += 1
    console.log(item)
})

/**
 * 例子二: 封装一个filter 方法
 * filter 方法是,遍历一个数组,会把符合条件的元素放到一个空数组里面,然后返回整个数组
 *  但是由于条件处理是灵活变化的,因此使用回调函数可以解决
 */

function filter(array, fn) {
    // 定义接收符合元素的数组
    let result = []
    for(let i = 0; i < array.length; i++) {
        // 判断如果如何传入进来的条件 并且把每个元素传递给函数参数
        if(fn(array[i])) {
            result.push(array[i])
        }
    }
    return result
}

let arr2 = [2, 4, 8, 9]
let res = filter(arr2, function(item) {
    return item % 2 === 0
})
console.log(res)

// 总结:让函数作为参数的好处,可以让函数变得灵活,不需要考虑内部是如何实现的
// 高阶函数 - 函数作为返回值
// 让函数作为一个返回值,即让一个函数 生成 一个函数

function makeFn() {
    let msg = 'hello function'
    return function() {
        console.log(msg)
    }
}
// let fn = makeFn()
// fn()
makeFn()()

/**
 * 例子: 模拟只能支付一次的函数
 * 
 * 参数是一个参数~ 因为支付的金额是不确定的,并且调用返回的参数时,可能需要传参
 * 因此使用apply 接收返回参数调用时传递的参数
 */

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}元`)
})
pay(5)
pay(5)
pay(5)
使用高阶函数的意义
  • 抽象可以帮我们屏蔽细节,只需要关注我们的目标
  • 高阶函数是用来抽象通用的问题
// 面向过程的方式
let arr = [1, 3, 5, 7]
for(let i = 0; i < arr.length; i++) {
	console.log(arr[i])
}

// 高阶函数
function forEach(arrary, fn) {
	for(let i = 0; i < arrary.length; i++) {
  	fn(array[i])
  }
}

// 调用forEach 可以做打印每个数组元素、使每个数组元素+1 ...等等操作
forEach(arr, item => {
	console.log(item)
})
forEach(arr,item => {
  item += 1
})

function filter(array, fn) {
	let result = []
  for(let i = 0; i < array.length; i++) {
  	if(fn(array[i])) {
    	result.push(array[i])
    }
  }
	return result
}
// 调用filter方法,可以筛选出符合偶数的每个元素、符合小于5的元素...
filter(arr, item => item % 2 === 0)
filter(arr, item => item < 5)
模拟高阶函数

模拟常用的高阶函数:map、every、some

/**
 * map 作用:对数组中的每个元素进行遍历,
 * 并对每个元素进行处理,然后把处理的元素存储到一个新的数组中返回
 */

// 参数array:数组  fn:处理每个元素的方法
const map = (array, fn) => {
    // 定义新的数组 接收处理的结果
    let result = []
    // 遍历数组的每个元素
    for(let item of array) {
        // 把每个元素传递给 传过来的函数参数
        // 把处理的元素存储到 result 
        result.push(fn(item))
    }
    // 返回新数组
    return result
}
// 测试
let arr1 = [1, 2, 3, 4]
arr1 = map(arr1, item => item * item)
console.log(arr1)
// 总结:把函数作为参数,可以指定函数的方式对数组中的每个元素做任意的求值,使得map更灵活
/**
 * every 作用:判断数组中的每个元素是否都匹配我们指定的一个条件
 * 因为条件是灵活的,可变化的,因此使用函数做为参数
 */
// array: 数组  fn:自定义指定的条件
const every = (array, fn) => {
    // 先假设所有的元素都是匹配条件的
    let flag = true
    // 遍历数组 找到数组的每个元素 通过我们传递的条件方法fn 检测是否匹配该条件
    for(let item of array) {
        // 把执行的结果 赋值给flag 记录
        flag = fn(item)
        // 如果有一个不符合条件 则可以立即停止
        if(!flag) break
    }
    return flag
}
let arr2 = [10, 11, 13]
console.log(every(arr2, item => item > 9))
/**
 * 总结: 把函数作为参数,可以指定函数的方式对数组中的每个元素是否匹配我们指定的
 * 任意条件,使得every更灵活
 */
/**
 * some 作用:判断数组中是否有一个元素满足指定的条件
 * 因为条件是灵活的,可变化的,因此使用函数做为参数
 */
// array: 数组  fn:自定义指定的条件
const some = (array, fn) => {
    // 假设所有元素都不满足指定的条件
    let flag = false
    // 遍历数组 找到数组的每个元素 通过我们传递的条件方法fn 检测是否匹配该条件
    for(let item of array) {
        // 把执行的结果 赋值给flag 记录
        flag = fn(item)
        // 如果有一个符合条件 则可以立即停止
        if(flag) break
    }
    return flag
}
let arr3 = [2, 7, 9]
console.log(some(arr3, item => item % 2 === 0))
/**
 * 总结: 把函数作为参数,可以指定函数的方式对数组中的每个元素是否匹配我们指定的
 * 任意条件,使得some更灵活
 */

通过把一个函数传递给另一个函数,使得这个函数变得更加灵活

闭包

闭包(closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员.
  • 闭包的本质:函数在执行的时候会放在一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问到外部函数的成员.
// 正常的函数
function makeFn() {
	let msg = 'hello msg'
}
const fn = makeFn()
// 正常来说,当makeFn函数执行完毕之后,里面的内部成员也会被释放掉
// 函数作为返回值
function makeFn () {
	let msg = 'hello msg'
  return function() {
  	console.log(msg)
  }
}
const fn = makeFn()
fn()
/*
	如果makeFn函数里面,返回了一个函数,并在返回的函数内部又访问了外部函数中的成员,那其实这就是闭包。
  现在调用完makeFn函数之后,会返回一个函数,那么其实这个fn就是引用了makeFn返回的函数,也就是外部
  对内部的成员有引用,那当外部对内部有引用的时候,那么此时makeFn内部的成员就不能被释放,调用fn,
  也就是调用内部函数,当调用内部函数的时候,会访问到msg也就是makeFn中的变量
  
  比如在makeFn相同的作用域中,去调用了makeFn中的内部函数,当调用内部函数的时候,可以访问到makeFn函数中的
  这个作用域的成员。第一点:在另一个作用域中,可以调用makeFn中的内部函数;第二,当调用内部函数的时候,可以
  访问到makeFn函数的内部成员,因此闭包的核心作用,就是把makeFn中内部内部成员的作用范围延长了。
  因为正常情况,makeFn执行完毕后,内部成员会被释放掉,但如果makeFn返回了一个成员,并且外部对这个成员
  有引用,那么此时这个makeFn这些内部成员在执行完毕后不会被释放
*/
// 闭包的例子
function once (fn) {
	let done = false
  return function() {
  	if(!done) {
    	return fn.apply(this,arrguments)
    }
  }
}
let pay = once(function(money) {
	console.log(`支付了${money}元`)
})
pay(5)
pay(5)

/*
	通过pay指向once返回的函数,也就是外部作用域对one函数内部有引用,因此once函数执行完毕后,done不会
  被释放掉。当再次调用pay时,pay其实就是once返回的内部函数,当执行这个内部函数可以访问到外部的变量done
  
  正常情况下,once函数如果没有返回一个函数,那么它会被放到一个执行栈上,当函数执行完毕后这个函数会在
  执行栈上移除,并且内部的变量也会从内存中移除。但是如果外部对once返回的函数有引用的话,once函数执行
  完毕后,会从执行栈上移除,但是因为外部对内部的成员有引用,所以它内部的成员不会从内存中移除
*/
// 闭包的案例

// 求一个数的平方,幂方,四次方
/*
    Math.pow(2, 2)
    Math.pow(3, 2)
    Math.pow(4, 2)
    这样子写,会导致不同的数求相同的平方要写多次
*/

// 定义一个求平方的方法
function makePower(pow) {
    return function(num) {
        // 此时的内部函数对外部函数的参数有引用 形成了闭包
        return Math.pow(num, pow)
    }
}

let power2 = makePower(2)  // 求平方
let power3 = makePower(3)  // 求立方

console.log(power2(2))
console.log(power2(3))
console.log(power3(3))


/*
    求员工的工资 基本工资 + 绩效
    getSalary(15000, 2000)
    getSalary(15000, 3000)
    getSalary(12000, 3000)

    这样子写会导致统一级别的员工基本工资会重写
*/

// 定义一个不同级别求工资的方法
function makeSalery(base) {
    // 内部的函数对外部函数的参数有引用
    return function(jx) {
        return base + jx
    }
}

let salayLevel1 = makeSalery(12000)  // 基本工资是12000的员工
let salayLevel2 = makeSalery(15000)  // 基本工资是15000的员工
console.log(salayLevel1(2000))
console.log(salayLevel1(3000))
console.log(salayLevel2(3000))

纯函数

纯函数的概念
  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
    • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
  • lodash 是一个纯函数的功能库,提供了对数组,数字,对象,字符串,函数等操作的一些方法
  • 数组的slice和splice分别是 纯函数 和 不纯的函数
    • slice 返回数组中的指定的部分,不会改变原数组
    • splice 对数组进行操作返回该数组,会改变原数组
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
let numbers = [1, 2, 3, 4, 5]
// 纯函数
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
// 总结: slice根据相同的输入始终得到相同的输出, 所以slice是纯函数

// 不纯的函数
numbers.splice(0, 3)
// => [1, 2, 3]  此时的numbers只剩下 [4, 5]
numbers.splice(0, 3) 
// => [4, 5]  此时的numbers 只剩下 []
numbers.splice(0, 3)
// => []
// 总结:splice根据相同的输入得到的输出是不同的,因此splice是不纯的函数

// 纯函数
function getSum(n1 + n2) {
	return n1 + n2
}
console.log(getSum(1, 2))
console.log(getSum(1, 2))
console.log(getSum(1, 2))
纯函数的好处
  • 可缓存
    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果存储起来
      比如有一个函数,这个函数执行起来特别耗时,需要多次去调用,那么每次去调用这个函数的时候,都需要等一段时间才能获取到这个结果,所以对性能来说是有影响的,想要提高性能,可以在这个函数第一 次执行的时候,当它执行完毕后,可以把这个结果缓存起来,当第二次调用这个函数的时候,不需要等待这么长的时间,而直接从缓存中获取结果,从而提高性能.
// lodash 提供了一个带记忆功能的函数 ---> memoize
const _ = require('lodash')

// 求圆面积的方法
function getArea(r) {
    console.log(r,"r")
    return Math.PI * r * r
}

/*
    memoize  接收一个参数纯函数,内部回对这个纯函数进行处理,把纯函数的结果进行缓存,
    并且memoize会返回一个带有记忆功能的函数
*/

let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))  // 后面这两个方法是直接获取缓存的结果 因此只打印了一次
console.log(getAreaWithMemory(4))
  • 自己模拟一个memoize 函数
// 分析:调用memoize 需要传递一个函数(纯函数),并且会返回一个函数
function memoize(fn) {
    /*
        这里需要定义一个对象,需要把函数执行的结果存储起来
        等下次调用相同的函数,把缓存的结果取出来
        因为纯函数是相同的输入会有相同的输出
        可以把函数的参数作为对象的键,把函数的执行结果作为对象的值
    */
   let cache = {}
    return function() {
        // 将形参arguments 转成字符串 作为键
        let key = JSON.stringify(arguments)
        // 判断当前是否有缓存的结果,如果有缓存的结果 直接取出来,如果没有则要执行fn这个函数
        cache[key] = cache[key] || fn.apply(this, arguments)
        console.log(cache,"cache")
        return cache[key]
    }
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))  // 后面这两个方法是直接获取缓存的结果 因此只打印了一次
console.log(getAreaWithMemory(5))
  • 可测试
    • 纯函数让测试更方便
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)

函数的副作用

纯函数:对于相同的输入永远会得到相同的输出,而且没有任何观察的副作用

// 不纯的
let mini = 18
function checkAge(age) {
	return age >= mini
}
// 当全局的变量mini发生改变时,会导致checkAge输入相同的结果会得到不同的输出

// 纯的 (有硬编码,后续可以通过柯里化解决)
function checkAge(age) {
	let mini = 18
  return age >= mini
}

副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用来源:

  • 配置文件
  • 数据库
  • 获取用户的输入

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

柯里化 (Haske!!Brooks Curry)

  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接收剩余的参数,返回结果
// 判断年龄是否大于基准值

// 硬编码写法
function checkAge(age) {
    let min = 18  // 把基准值定义成一个具体数字,这是硬编码
    return age >= min
}

// 解决硬编码,把基准值提取出来作为参数

// 这是普通的纯函数,相同的输入会有相同的输出
function checkAge(min, age) {
    return age >= min
}

// 假设要经常用到基准值18,那么代码中的18就会被经常重复
checkAge(18, 20)
checkAge(18, 25)
// 柯里化演示

// 如何避免基准值重复 ?
/*
    可以把基准值放到checkAge的参数中固定下来,然后age参数放到checkAge返回的函数中
    这种形式其实就是柯里化
    当函数有多个参数的时候,可以调用一个函数只传递部分的参数
    并且该函数返回一个新的函数,这个新的函数会接收剩余的参数
    并且返回相应的结果
*/

function checkAge(min) {
    return function(age) {
        return age >= min
    }
}

let checkAge18 = checkAge(18)   // 创建一个18的基准值
console.log(checkAge18(20))
console.log(checkAge18(17))

// 使用es6的写法
const checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)   // 创建一个18的基准值
console.log(checkAge18(20))
console.log(checkAge18(17))
lodash中的柯里化
  • _.curry(func)
    • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果,否则继续返回该函数并等待接收剩余的参数
    • 参数:需要柯里化的函数
    • 返回值:柯里化后的函数
// lodash中提供的柯里化
// 柯里化是可以把多元函数最终转成一个一元函数
const _ = require('lodash')

function getSum(a, b, c) {
    // 如果有三个参数则是三元函数,两个则是二元函数,一个则是一元函数
    return a + b + c
}

const curried = _.curry(getSum)

// 如果传入getSum所有的参数,则立即调用执行,并返回结果
console.log(curried(1, 2, 3))
// 如果传入getSum的部分参数,此时curry会返回一个函数等待接收剩余的参数
console.log(curried(1)(2, 3))
// 转成一元函数
console.log(curried(1,2)(3))
  • 案例
// 柯里化(颗粒化) 小案例
// 匹配字符串的空格 或是 数字

/**
 * ''.match(/\s+/g)
 * ''.match(/\d+/g)
 * 这种是面向过程的写法,代码不能重复使用
 * 
 * 使用函数式编程 能使代码复用
 */

// reg - 正则表达式  str字符串
function match(reg, str) {
    return str.match(reg)
}

let res1 = match(/\s+/g,'hello world')
let res2 = match(/\s+/g,'hello lili')
console.log(res1)
console.log(res2)
/*
    但是这种写法会导致 如果我想验证不同字符串是否有空格 
    那么这个正则表达式就会重复的写多次
    因此可以使用柯里化的方法,让一个函数生成一个新的函数
    使得这个正则表达式的参数不重复
*/
// 柯里化处理
const _ = require('lodash')

const match = _.curry(function(reg, str) {
    return str.match(reg)
})
const haveSpace = match(/\s+/g)   // 生成匹配空格的方法
const haveNumber = match(/\d+/g)  // 生成匹配数字的方法

console.log(haveSpace('hello world'))  // [ ' ' ]  
console.log(haveNumber('hello12323'))  // [ '12323' ]


// 处理数组 过滤出匹配条件的元素
// fn- 处理数组匹配的方法   array - 数组
const filter = _.curry(function(fn, array) {
    return array.filter(fn())
})

// es6的写法
const filter = _.curry((fn, array) => array.filter(fn()))

// 柯里化的函数 如果传递全部参数 则是直接返回结果
console.log(filter(haveSpace, ['张 三','李四']))  // [ '张 三' ]
// 但可以更颗粒化函数 可以生成专门的匹配数组空格的方法
const findSpace = filter(haveSpace)
const findNumber = filter(haveNumber)
console.log(findSpace(['小 红', '小黑']))  // [ '小 红' ] 
console.log(findNumber(['小 红', '小黑123']))  // [ '小黑123' ]
柯里化实现原理
// 模拟柯里化实现原理
// 模拟实现lodash中的 curry 方法
/**
 * 回顾curry 方法是如何调用的
 * 调用curry时,需要传递一个函数参数,当调用完成时,curry会返回一个函数
 * 这个返回的函数是一个柯里化后的函数
 * 
 * 分析curry返回柯里化函数的调用形式
 * 假设传入了getSum的全部参数,则会立即执行getSum方法并返回结果
 * 假设传入了getSum的部分参数,那么此时curried会返回一个新的函数,等待接收getSum剩余的参数
 */
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))
// 模拟开始
// 需要的参数是一个函数(是一个需要进行柯里化的函数)
function curry(func) {
  // curry函数执行完毕后 会返回一个新的函数
  // 使用es6的剩余参数写法来接收 实际传入的参数
  // 想要知道函数作为参数时携带的形参个数 可以通过函数.length来获取
  return function curriedFn (...args) {
    // 判断实参 和 形参
    if (args.length < func.length) {
      /*
          如果调用curry方法返回的函数传递的实参小于需要柯里化的函数的形参个数
          那么就是属于第二种调用形式,需要返回一个新的函数,等待接收剩余的参数
      */
      return function () {
        /*
          分析
          如果第一次传参传了1,那么就是调用了最外层的curriedFn这个函数,...args = 1
          第二次传递的参数是2,3 则是调用了最内层的function,可以使用arguments获取实参
          这时候需要把参数合并起来,再调用最外层得curriedFn,这样就能使得传递得参数和
          需要柯里化得参数一致,就能走下一个判断,执行需要柯里化得函数
        */
        return curriedFn(...(args.concat(Array.from(arguments))))
      }
    }

    /**
     * 如果调用curry方法返回的函数传递的实参大于等于需要柯里化的函数的形参个数
     * 那么就是属于第一种调用形式,直接调用需要柯里化的函数并传递参数
     */
    return func(...args)
  }
}

function getSum(a, b, c) {
  return a + b + c 
}

let curried = curry(getSum)

console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
柯里化总结
  • 柯里化可以让我们给一个函数传递较少的的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的缓存 (使用了闭包)
  • 让函数变的更灵活,让函数的粒度更小
  • 可以把多元函数转化成一元函数,可以组合使用函数产生强大的功能

函数组合

前提
  • 纯函数和柯里化很容易写出洋葱代码h(g(f(x)))
    • 获取数组的最后一个元素再转换成大写字母,.toUpper(.first(_.reverse(array)))
      在这里插入图片描述
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新得函数
    在这里插入图片描述
函数组合
  • 函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程得函数合并成一个函数
    • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
    • 函数组合默认是从右到左执行
// 函数组合演示
// 例子:使用函数组合,取数组中最后一个元素

// 定义一个函数,这个是把多个函数组合成一个新的函数
// 接收多个函数参数  并且组成一个新的函数返回
function compose(f, g) {
  //  返回的这个函数需要接受一个参数(这个参数是我们输入的)
  return function(value) {
    /*
      处理这个输入,并且把处理的结果返回
      函数组合的顺序是 从右到左依次执行
      即把参数传递给g,把g处理的结果传递给f,f处理的结果就是我们要返回的最终结果
    */
    return f(g(value))
  }
}

// 先把数组进行反转,再取反转后的数组的第一个元素

// 接下来定义一些细粒度的函数:数组反转的函数,获取数组第一个元素的函数

function reverse(array) {
  return array.reverse()
}

function first(array) {
  return array[0]
}


// 调用compose生成一个新的函数,注意顺序
const last = compose(first,reverse)
console.log(last([1, 2, 3]))  // 3

/*
  总结函数式组合
  函数式组合,可以把多个函数组合成一个新的函数
  执行的过程中,把参数输入给执行的第一个函数,当它执行完后会返回一个中间结果,
  并且把这个中间结果传递给下一个函数进行处理,当最后一个函数执行完毕后,会把最终的结果返回
*/
lodash 中的组合函数
  • lodash中的组合函数flow()或者flowRight( ),他们都可以组合多个函数
  • flow( )是从左到右运行
  • flowRight( )是从右到左运行,使用的更多一些
// lodash 中的函数组合的方法  _.flowRight()

const _ = require('lodash')

const reverse = array => array.reverse()  // 反转数组的方法
const first = array => array[0]   // 获取数组的第一个元素
const toUp = str => str.toUpperCase()  // 将字符串转成大写

const f = _.flowRight(toUp,first,reverse)
console.log(f(['lily','cici','lulu']))  // LULU
函数组合实现原理
// 模拟lodash中的flowRight
/*
  分析:flowRight的参数是不固定的,并且这些参数都是函数
  调用完flowRight之后,会把这些参数的函数组合成一个新的函数返回
  并且返回的函数要接收一个参数(输入的参数)
*/

// 由于不确定传递多少个参数  可以使用剩余参数来表示
function compose(...args) {
  // 要返回一个函数 并且需要接收一个参数
  return function(value) {
    /*
      要依次调用传过来的函数
      因为函数组合是默认从右到左执行的
      因此调用args存储的函数想要从后往前执行,需要先将args反转
      反转后,就要调用args中存储的每个函数,每个函数要对value进行相应的处理
      并且把值依次累计,最后返回
    */
    return args.reverse().reduce(function(acc,fn) {
      // acc是每次累计的值 fn是每个需要执行的函数
      // value是初始值,当添加了初始值,那么一开始acc就等于value
      // 把每次累计的值传给fn
      return fn(acc)
    },value)
  }
}

const reverse = array => array.reverse()   // 反转数组的方法
const first = array => array[0]    // 获取数组的第一个元素
const toUp = str => str.toUpperCase()  // 将字符串转成大写
const f = compose(toUp,first,reverse)
console.log(f(["john","lili","zhang"]))
// 使用es6的写法
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const reverse = array => array.reverse()   // 反转数组的方法
const first = array => array[0]    // 获取数组的第一个元素
const toUp = str => str.toUpperCase()  // 将字符串转成大写
const f = compose(toUp,first,reverse)
console.log(f(["john","lili","zhang"]))
函数组合结合律
  • 函数的组合要满足结合律(associativity):
    • 我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
// 函数组合的结合律
const _ =require('lodash')
// const f = _.flowRight(_.toUpper,_.first,_.reverse)
// 可以结合前两个函数 或者去 组合后两个函数  结果都是 一样的
// const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse))
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)

console.log(f(['one','two','three']))  // TRHEE
函数组合如何调试
// 函数组合如何调试
/**
 * 如果函数执行的结果和我们的预期不一样 我们该如何调试呢 ?
 */

/*
    将 NEVER SAY DIE  ---->  never-say-die
    先使用空格将字符串切割 -> 切割完后转成小写 -> 最后使用join进行分割
*/
const _ = require('lodash')
// 因为使用组合函数的时候,我们只需要的是一元函数
// 因此借助柯里化,把lodash的一些函数,进行二次改造成一元函数

// 因为lodash的split方法有多个参数,需使用柯里化转成一元函数
// 因为传值是在调用函数式组合的时候才传值,因此把值写在最后一个位置
const split = _.curry((sep, str) => _.split(str, sep))

// 因为lodash的toLower方法只有一个参数  可直接使用

// 因为lodash中的join方法有多个参数,需使用柯里化转成一元函数
const join = _.curry((sep, array) => _.join(array, sep))

/**
 * 因为split只传了一个参数,因此会发返回一个新的函数
 * split切割完后,会把值传给toLower
 * toLower转成小写之后,会传递给join
 */

const f = _.flowRight(join('-'),_.toLower,split(' '))
console.log(f('NEVER SAY DIE'))   // n-e-v-e-r-,-s-a-y-,-d-i-e

// 预期的结果不一样 需要调试
// 预期的结果不一样 需要调试
const _ = require('lodash')

// 这是一个可以打印出处理结果后的值的方法
const log = v => {
    console.log(v)
    return v    // 记得要return出去传递给下一个函数
}

const split = _.curry((sep, str) => _.split(str, sep))

// 因为lodash中的toLower方法只有一个参数,可以直接使用

const join = _.curry((sep, array) => _.join(array, sep))

// 可以在函数的后面,写一个方法打印出处理后的结果
const f = _.flowRight(join('-'), _.toLower, log ,split(' '))

// 此时log方法会打印出split函数处理的结果 [ 'NEVER', 'SAY', 'DIE' ]
console.log(f('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((sep, str) => _.split(str, sep))

// 因为lodash的toLower方法只有一个参数 可直接使用

// 因为lodash中的join方法有多个参数,需使用柯里化转成一元函数
const join = _.curry((sep,array) => _.join(array,sep))

// 可以在函数的后面,写一个方法打印出处理后的结果
const f =  _.flowRight(join('-'),trace('toLowder之后的数据'), _.toLower, trace('split之后的数据'), split(' '))
console.log(f('NEVER SAY DIE'))

/**
 * trace 会打印出
 * split之后的数据 [ 'NEVER', 'SAY', 'DIE' ]
 * toLowder之后的数据 never,say,die
 * 这时候就会找到问题在于toLower这里,因此要传递给join的是一个数组,此时是一个字符串
 * 
 * 解决,需要遍历数组,将每个元素转成小写,因此想到map,map可以对数组的每个元素进行处理
 */
// 最后改造
const _ = require('lodash')
// 因为使用函数组合的时候,我们只需要的是一元函数
// 因此借助柯里化 把lodash的一些函数 进行二次改造成一元函数

// 因为lodash的split方法有多个参数,需要使用柯里化转成一元函数
// 因为传值是在调用函数式组合的时候才传值,因此把值写在最后一个位置

// 这是一个可以打印出处理结果后的值的辅助函数
// 但是这种写法没有标记,难以分清是哪个函数打印出来的
// const log = v => {
//     console.log(v)
//     return v
// }

// 加上标记后的辅助函数
const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})

const split = _.curry((sep, str) => _.split(str, sep))

// 因为lodash的toLower方法只有一个参数 可直接使用

// 因为lodash中的join方法有多个参数,需要使用柯里化转成一元函数
const join = _.curry((sep, array) => _.join(array, sep))

// 因为lodash中的map方法有多个参数,需要使用柯里化转成一元函数,fn是决定如何处理数组中的每一项
const map = _.curry((fn, array) => _.map(array, fn))

/**
 * 因为split只传了一个参数,因此会返回一个新的函数
 * split切割完之后,会把值传递给toLower
 * 但注意如果只使用toLower处理则只会返回字符串,join接收的是一个数组
 * 因此使得map方法对数组的每个元素进行转换成小写的方法
 * 转成小写之后,会传递给join
 */

// 可以在函数的后面,写一个方法打印出处理后的结果
const f = _.flowRight(join('-') ,trace('toLower之后的数据'), map(_.toLower), trace('split之后的数据'),split(' '))
console.log(f('NEVER SAY DIE'))  // never-say-die

上述例子中,会使用到lodash的一些方法,但方法有多个参数的时候,我们需要二次改造成只有一个参数的,会有些麻烦,这里lodash提供了另一个模块可以解决。

lodash中的FP模块

lodash / fp
  • lodash 的 fp模块提供了实用的对函数式编程友好的方法
  • 提供了不可变auto-curried iteratee-first data-last 方法
/**
 * lodash的方法都是数据优先 函数之后
 * 在使用函数式组合时,会用到lodash的一些方法,但如果方法有多个参数
 * 需要我们使用柯里化二次改造方法 会有些麻烦
 * 
 * lodash 提供了另一个fp模块,里面的方法都是经过柯里化处理的
 * 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
lodash和lodash/fp模块 中map方法的区别
// lodash 和 lodash/fp模块中 map方法的区别

// lodash 中的map方法
const _ = require('lodash')
console.log(_.map(['23','8','10'],parseInt))  // [ 23, NaN, 2 ]
/**
 * 分析:
 * lodash中map方法的所接收的函数的参数是3个
 * 第一个是要处理的每一个元素,第二个是索引,第三个是数组
 * 即 parseInt('23', 0, array)
 *    parseInt('8', 1, array)
 *    parseInt('10', 2, array)
 * 注意,parseInt如果传递了第二个参数,第二个参数表示要解析的数字的基数。该值介于2 ~ 36之间
 * 如果省略该参数或其值为0,则数字将以10为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
 * 如果该参数小于2 或者大于36,则parseInt()将返回NaN
 */


// lodash/fp模块中的map方法
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10']))  // [ 23, 8, 10 ]

/**
 * 分析:
 * lodash/fp模块中的map方法所接收的函数的参数只有一个
 * 也就是当前处理的元素
 */

Point Free

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
/**
 * point free 是一种编程风格
 * 把运算的过程 合并成一个新的函数
 * 在过程中是不需要指明所用到的数据
 */

/**
 * 例子:Hello    World   =>  hello_world
 * 需要用到  转成小写、替换 这两个方法
 */

const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)
console.log(f('Hello    World'))  // hello_world
// point free 案例
// world web wild   ==> W.W.W
/**
 * 分析:
 * 将字符串根据空格 分割成数组
 * 遍历数组把每个元素变成大写
 * 提取每个元素的第一个字母
 * 把数组按照指定的分割号转成字符串
 */

const fp = require('lodash/fp')

// 这种写法会使得数组需要遍历两次
// const f = fp.flowRight(fp.join('.'), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))

// 改造
const f = fp.flowRight(fp.join('.'),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '))
console.log(f('world web wild'))  // W.W.W
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值