为什么要学习函数式编程
- 函数式编程是随着React的流行受到越来越多的关注
- Vue3也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的利用tree shaking过滤无用的代码
- 方便测试,方便并行处理
- 有很多库可以帮组我们进行函数式开发(例如下面3个库)
-
lodash(https://www.lodashjs.com/)是一个一致性、模块化、高性能的 JavaScript 实用工具库。 -
unnderscore(https://underscorejs.net/)是一个JavaScript实用库,提供了一整套函数式编程的实用功能,但是没有扩展任何JavaScript内置对象 -
ramda(https://ramda.cn/)一款实用的 JavaScript 函数式编程库,专门为函数式编程风格而设计,更容易创建函数式 pipeline、且从不改变用户已有数据。
编程范式(相信绝大部分开发人员、DBA都听过范式这个词,在MySQL中有第一范式、第二范式、第三范式、BCNF范式等,在开发中也有相应的范式,专业词汇叫编程范式(ProgrammingParadigm))如下图
主要说明下以下编程方式
- 函数式编程 -> 把现实的事物与事物之间的联系,抽象到程序世界(对运算过程进行抽象)
- 面向过程编程 -> 按照步骤来实现,一步一步的实现我们想要的功能
- 面向对象编程 -> 把现实中的事物抽象成类和对象,通过继承封装和多态来演示事物事件的联系(抽象现实中的事物)
函数式编程的函数不是程序中的函数(方法),而是数学中的映射方法的关系。例如y = sin(x) x和y 的关系。始终保持相同的输入,始终要得到相同的输出(纯函数的概念)函数式编程就是用来描述数据(函数)直接的映射。好处,可以让代码重复使用 ,而且抽象出来柯里化的函数,还可以根据自己的组合,组合成功能更大的函数
举个简单的例子
例1:面向过程的函数编程
```js
let a = 1
let b = 2
let c = a + b
console.log(c)
```
例2:函数式编程
```js
function add (a , b){
return a + b
}
let a = add(1,3)
console(a)
```
函数是一等公民(MDN中翻译成头等函数)
1. 函数可以储存在变量中 2. 函数可以作为参数 3. 函数可以作为返回值
- 在js中,函数就是一个普通的对象,通过new function().我们就可以把函数出存在变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new function(alert(1))来构造新的函数
把一个函数的方法赋值给另一函数或者方法,是相等的。例如下面代码
const test = {
index(arg):{return Views.index(arg)}
}
//优化
const test = {
index: Views.index
}
高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以吧函数作为另一个函数的的返回结果
- 函数作为参数 (可以使函数更加灵活)
例如自己写一个高级函数(foreach,filter,map,every)这些高阶函数就是函数作为返回值的
//自己封装的foreach
function foreach (array , fun){
for(i=0;i<array.length;i++){
fun(array[i])
}
}
foreach([1,2,3],(e)=>{
console.log(e)
})
//自己封装的filter,例如我要找到大于10的数组
function filter (array , fun){
let result = []
for(i=0;i<array.length;i++){
if(fun((array[i]))){
result.push(array[i])
}
}
return result
}
let list = filter([1,20,3,40],(item)=>{
return item>10 ? true : false
})
console.log(list)
//自己封装的map,只能循环数组,每个数字+1
function map (array , fun){
let result = []
for(let value of array){
result.push(fun(value))
}
return result
}
let list = map([1,20,3,40],item => item+1)
console.log(list)
//自己封装的every,是否全部大于0
const every = function(array , fun){
let result = true
for(let value of array){
if(!fun(value)){
result = false
break
}
}
return result
}
let list = every([1,20,3,40],item => item>0)
console.log(list)
- 函数作为返回值
function makeFun(){
let msg = ‘hellow’
return function(){
console.log(msg)
}
}
//返回的是一个函数
const fun = makeFun()
//执行函数
fun()
//或者直接执行
makeFun()()
//自己封装的once
function once(fun){
let done = false
return function(){
if(!done){
done = true
return fun.apply(this,arguments)
}
}
}
let list = once(e=>{
console.log(`花了${e}元`)
})
list(5)
list(5)
list(5)
高阶函数的意义
- 抽象可以帮助我们屏蔽细节,只需要我们关注目标
- 高阶函数是用来抽象通用的问题 ,可以让我们很灵活
闭包
- 可以在另一个作用域中调用一个函数内部函数并访问到函数内部作用域的成员
- 闭包的本质:函数在执行的时候会放在执行栈上,当函数执行完毕之后会从执行栈移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数成员
//例如如下代码
let makeFun = function(){
let msg = 'hellow'
return function(){
console.log(msg)
}
}
console.log(makeFun()())
现在使用闭包封装一个幂函数 10的2次幂
function pow(y){
return function(x){
return Math.pow(x,y)
}
}
let a = pow(2)
let b = a(10)
console.log(b)
纯函数(数学中函数的概念)
- 纯函数:相同的输入永远会得到相同的输出,y=f(x) 例如f就是其中的过程
-
lodash(www.lodashjs.com/)是一个一致性、模块化… JavaScript 实用工具库。是一个纯函数的功能库,提供了对数组,字符串,函数的一些方法
-
例如数组中的slice 和 spliec 分别是纯函数和不纯函数。因为splice改变了原数组
//例如写一个纯函数
function add (a,b){
return a+b
}
//永远都会是一个结果
console.log(1,3)
** 函数式编程不会保留计算机中间的结果,所以变量是不可变的(无状态) 我们可以把一个函数执行结果交给另一个函数出处理 **
lodash的简单使用
const _ = require('lodash')
const arr = ['d','r','3','dd','kk']
console.log(_.first(arr))
console.log(_.last(arr))
使用纯函数的好处
const _ = require('lodash')
//memoize 记忆纯函数(带记忆的函数)
//计算圆的面积
function geArea(r){
console.log(r)
return Math.PI * r *r
}
let getAreaM = _.memoize(geArea)
console.log(getAreaM(4))
console.log(getAreaM(4))
// 4
// 50.26548245743669
// 50.26548245743669
- 可以缓存
- 可测试(纯函数方便测试)
- 并行处理
- 在多线程环境下并行操作共享的内存可能会出现意外情况(多个线程同时修改一个变量)
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(web worker是多线程,js是单线程,但是也可以多线程)
//不纯函数
let mini = 10
function age (num){
return age
}
//因为let mini = 18是全局变量,如果收到污染,就会可能改变结果,所以放到函数内部的时候,就变成了纯函数
//纯函数(硬编码,可以通过柯里化解决)
function age (num){
let mini = 10
return age
}
- 副作用让一个函数变得不纯,纯函数是根据相同的输入返回相同的输出,如果函数依赖外部的状态就无法保证输出来源,就会产生副作用。
- 副作用来源:
配置文件 数据库 获取用户输入 等外部因素
- 副作用是不可避免的,所以我们尽可能的控制在可控范围内
柯里化
函数柯里化:当我们函数有多个参数的时候,可以对函数进行改造,调用一个函数,只传递部分参数(这部分参数永远不变呢),并且让这 个函数返回新的函数,新的函数去接受剩余的参数并且返回相应的结果
//柯里化函数
//此时min就是属于永远不变的参数
let checkAge = min => (age => age >= min)
let chec20 = checkAge(20)
console.log(chec20(19))
- lodash中的柯里化函数使用(_.curry()=>创建一个函数,此函数可以传递一个或者多个fun函数,所需要的参数都执行了,否则接续等待接收参数,执行完成之后返回一个柯里化的函数)
柯里化总结
- 柯里化可以让我们生产一个新的函数,并且新的函数,已经记录了固定的参数,使用闭包即住了一些传递的参数,
- 是一种对函数的缓存,使用闭包对参数进行缓存
- 让函数更灵活,让函数颗粒度更小
- 可以把多元函数转成一元函数,可以组成使用函数产生强大的功能
函数组合结合率
函数结合率:把函数结合。不管是g和f,还是f和g,的的结果都是一样的
//下面就是相等的
const f = _.flowRight(_.toUpper,_.first,_.reverse)
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f = _.flowRight(_.toUpper,_.first,_.flowRight(_.reverse))