一、为什么要学习函数式编程
- 函数式编程随着react的流行受到越来越多的关注
- Vue3 开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好地利用tree shaking过滤无用代码
- 方便测试和并行处理
有很多库可以帮助我们进行函数式开发:lodash、underscore
二、概念
函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)程序的本质:根据输入,通过某种运算获得相应的输出,程序开发过程中会设计很多有输入和输出的函数
- x-> f(联系、映射)-> y,y=f(x)
- 函数式编程中的函数指的不是程序中的函数,而是数学中的函数(映射关系),如:y = sin(x) x和y的关系
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射
//非函数式
let num1 = 2
let num2 = 3
let sum = num1 + num2
//函数式
function add(n1, n2){
return n1 +n2
}
let sum = add(2, 3)
三、前置知识
1、函数是一等公民
- 函数可以存储在变量中
- 函数作为参数
- 函数作为返回值
在JS中函数就是一个普通对象,可以把它存储到变量/数组中,还可以作为另一个函数的参数和返回值,甚至可以在程序运行中通过new Function(‘alert(1)’)来构造一个新的函数
const BlogController = {
index(posts) { return Views.index(posts) },
show(posts) { return Views.show(posts) }
}
//优化
const BlogController = {
index: Views.index,
show: Views.show
}
2、高阶函数
- 高阶函数 (Higher-order function)
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
函数作为参数
// forEach
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
// filter
function filter (array, fn) {
let results = []
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
results.push(array[i])
}
}
return results
}
函数作为返回值
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn()
function once(fn){
let done = false
return function(){
if(!done){
return fn.apply(this, arguments)
}
}
}
let pay = once(money=>{
console.log(`支付${money}`)
})
pay()
使用高阶函数的意义
抽象可以帮我们屏蔽细节,只需要关注与我们的目标
- 高阶函数是用来抽象通用的问题
- 常见的高阶函数:
forEach、map、filter、every、some、find/findeIndex、reduce、sort…
const some = (array, fn){
let result = false
for(let item of array){
result = fn(item)
if(result){
break
}
}
return result
}
3、闭包
- 闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
- 闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
闭包案例
// 生成计算数字的多少次幂的函数
function makePower (power) {
return function (x) {
return Math.pow(x, power)
}
}
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4))
console.log(power3(4))
4、纯函数
- 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- 类似数学中的函数,用于描述输入和输出之间的关系 y = f(x)
不得改写参数数据
不会产生任何副作用,例如网络请求,输入和输出设备
不能调用Date.now()或者Math.random()等不纯的方法
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
数组的slice和splice分别是纯函数和不纯的函数
- slice返回数组中的指定部分,不会改变原数组
- splice对数组进行操作返回该数组,会改变原数组
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)let array = [1, 2, 3, 4, 5]
console.log(array.slice(0,3)) //[) 截取的位置索引
console.log(array)
console.log(array.splice(0,3))//第一个是索引,第二个是截取多少个
console.log(array)
我们可以把一个函数的执行结果交给另一个函数去处理
优点:
- 可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
function getArea(r){
console.log(r)
return Math.PI*r*r
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
//模拟
function memoize(f){
let cache = {}
return function(){
let key = JSON.stringify(arguments)
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
- 可测试
纯函数让测试更方便
- 并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行
副作用:
let mini = 18
function checkAge(age){
return age>= mini
}
function checkAge(age){
let mini = 18
return age>= mini
}
副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证,就会有副作用
副作用来源:
- 配置文件
- 数据库
- 获取用户的输入。。。
所有的外部交互都有可能带来副作用,使得方法通用性下降,不适合扩展和复用,给程序带来安全隐患和不稳定性,但副作用不能完全禁止,需尽可能控制在可控范围
四、lodash常用方法
first、last、toUpper、reverse、each、includes、find、findIndex
const _ = require('lodash')
const array = ['jack', 'tom', 'lucy', 'jhon']
console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.last(array)))
console.log(_.reverse(array))
const r = _.each(array,(item,index)=>{
console.log(item,index)
})
五、柯里化
当一个函数有多个参数的时候,现传递一部分参数调用它,然后返回一个新的函数接收剩余的参数,返回结果
function checkAge(age){
let min = 18
retuen age >= min
}
function checkAge(min,age){
retuen age >= min
}
function checkAge(min){
retuen function(age){
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge = min =>(age => age>=min)
lodash中的柯里化
_.curry
- 创建一个函数,接受一个或多个方法,如果方法所需参数都被提供,则执行方法并返回结果,否则继续返回该函数并等待接受剩余的函数
- 参数:需要柯里化的函数
返回:柯里化后的函数
function getSum(a, b, c){
return a+b+c
}
const curried = _.curry(getSum)
curried(1,2,3)
curried(1)(2)(3)
curried(1)(2,3)
例子
//function match(reg, str){
// return str.match(reg)
//}
const match = _.curry(function (reg, str){
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNum = match(/\d+/g)
const filter = _.curry(function (func,arr){
return arr.filter(func)
})
filter(haveSpace,['john connor','john_connor'])
const findSpace = filter(haveSpace)
findSpace(['john connor','john_connor'])
原理
function curry(func){
return function curriedFn(...args){
//判断实参和形参的个数
if(args.length < func.length){
return function(){
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
总结:
给一个函数传递较少的参数,得到一个已经记住了某些固定参数的函数
- 这是一种对函数参数的缓存
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转为一元函数,可以组合使用函数产生强大的功能