对于函数式编程个人一直不是很了解,于是抽空查看相关资料整理笔记下
1、概念
在编程思想上讲,目前有三类:命令式(面向过程)、面向对象、函数式,简单介绍下这三种
命令式编程 ———— 求和
let a = 10
let b = 20
let sum = a + b
可以看到是多个语句或者指令等让计算机执行一些动作
面向对象编程 ———— 把现实中的事物抽象成程序中的类和对象,通过封装、继承、多态来演示事物之间的联系
函数式编程 ———— 求和
function add(x,y) {
return x + y
}
add(a,b)
通过函数对数据进行了转换,类似于数学中的函数 y = sin(x),强调的是输入输出的一种映射关系;同时还可以不断的串联多个函数对数处理(xxx.split(’ ‘) .map(v => v.slice(0, 1).toUpperCase() + v.slice(1)).join(’ '))
2、函数式编程的部分特性
列举一些个人感觉不是很好理解的
无副作用
// 有副作用
let a = 10
function getSum(b) {
a++ // 修改了外部变量,类似的还有数组的 splice 会修改原数组
return a + b
}
// 没有副作用
function getSum(b) {
// 没有修改原数据
return a + b
}
透明引用
函数只会用到传递的参数或者内部固定的参数
// 根据id获取列表信息
function getList(id) {
// 只用参数id
axios(id).then(res => {
...
})
}
// 不透明的
let id = 10
function getList() {
// 用了外部的变量
axios(id).then(res => {
...
})
}
关于透明引用补充一些:在使用vue时,可能有些获取数据的函数是如下写法
methods: {
// 获取xxx列表
getList() {
// 函数依赖 this.id 这个变量,而不是参数/内部固定参数
axios.getList({
id: this.id }).then(res => {
...
})
},
// 透明引用
getList(id ) {
// 函数依赖传递的参数
axios.getList({
id }).then(res => {
...
})
}
},
上述透明引用,避免了无用变量的创建,id 完全可以通过传递的方式,而非在data中再定义一个,同时别人也无需进入getList函数,就可以知道该函数根据 id 获取列表
3、函数式编程的几种模式
高阶函数、闭包、纯函数、柯里化、函数组合,还有函子等概念(这个目前笔者还未曾遇到过,所以暂时跳过)
4、高阶函数
概念:
1、可以把函数作为参数传递给另外一个函数
2、可把函数作为另外一个函数的返回值
使用高阶函数,我们可以把一些处理编写成高阶函数,具体想要实现什么,由自己传入的函数决定
比如数组的 filter,要想过滤只需要按需求传入过滤的函数即可,相反,不使用这个高阶函数,是不是还是得for循环去每次判断? 当然这里不是想表达 filter 这些优于 for ,只是想表明高阶函数意义,不需要关注具体实现细节,只需要传入自定义条件,即可实现结果
比如数组的 map、filter、forEach等
我们不需要关心它内部如何实现
比如:想要过滤一些数据,就传递自己编写的过滤条件调用即可
函数作为返回值
// 编写一个只执行一次的函数,貌似也有闭包那味儿
function once(fn) {
let done = false
return function () {
if(!done) {
done = true
return fn.call(this,arguments)
}
}
}
let getNum = once(function() {
console.log('看我打印几次')
})
getNum() //看我打印几次
getNum() //不会执行
getNum()
高阶函数其实大家在实际项目中用到的也比较多,只是没发觉而已
5、闭包
概念:
简单来说,闭包的格式就是函数嵌套,内层函数对外层函数作用域仍有引用,导致外部函数变量不会被销毁。其实你经常在写闭包,只是没发现
// 闭包好处之一 作缓存
function cheackAge(base) {
return function (age) {
对 base 一直有引用
return age > base
}
}
//比如这种情况,需要对年龄是否大于 18 进行判断,我们可以写成闭包形式,
//首次调用时,会将基准值缓存起来
//再返回一个函数,且返回的这个函数无法对 基准 进行修改,最大程度保证了纯函数的特性
let is18 = cheackAge(18)
console.log(is18(20))
闭包这个话题,相信不用聊太多,毕竟各类面经谈的不少,所以这里不着重讲
6、纯函数
概念:
相同的输入,只会得到相同的输出,好比数学公式,S = π * r²,只要r一样,面积始终不会变
对于上例,完全可以写成一个普通比较函数 age > base,如果是这样 base 势必会从外界传入,既然是外界传入,就少不了被修改的风险,就不能百分之百保证函数是纯的
相反,我们通过闭包方式,第一此就将比较基准缓存下来,后续比较使用返回的新函数是不可能修改到比较的基准值(18),因此是纯的
//对于数组
slice --- 纯的(不会改变原数组,所以每次slice(0,3)得到结果相同)
splice -- 不纯(要改变原数组,所以每次调splice(0,3)结果都不一样)
//纯的 只要a b 相同,结果总是相同的
function getSum (a,b) {
return a + b
}
// ps 纯函数不会保留计算中间值,计算中也没有什么变量,不会影响结果,没有副作用
副作用补充:
我们说纯函数的作用就是,相同的输入只会得到相同的输出,如果无法保证这点,则这个函数不纯,也就是有副作用
副作用 — > 让函数变的不纯,因为结果会到(外部,或者内部)受影响
所以,当函数的状态 依赖外部作用无法保证相同输出时,就会带来副作用
比如: 配置文件、用户输入、vue 的watch、computed(都依赖外部变量)
纯函数,由于有相同的输入只会得到相同的输出特性,我们可以利用其作缓存(因为相同的输入,总是得到相同的输出,所以没必要再次计算)
// 利用纯函数 编写一个记忆函数
function memory(fn) {
const map = new Map()
return function (...arg) {
console.log(map)
let k = JSON.stringify(arg)
let value = map.get(k) || fn(...arg)
map.set(k, value)
return value
}
}
let memoredFoo = memory(foo)
console.log(memoredFoo(1,2,3))
7、柯里化
概念:
当一个函数有多个参数,可以先传递一部分参数调用它(这部分固定参数不变),然后返回一个已经记住某些固定参数的新函数,继续接收参数,
返回的函数会记住之前固定的参数,同时也是函数参数的一种缓存
多元函数转换为一元函数,可使用函数组合 组合成更强大的函数
//多元函数化解为一元函数
function foo(a, b, c) {
console.log('在计算了')
return a + b + c
}
function currie(fn) {
return function bar(...arg1) {
// 参数个数相等 返回值
if (fn.length == arg1.length) {
return fn.apply(this, arg1)
}
// 继续返回一个函数 接收参数
return function (...arg2) {
return bar.apply(this, arg1.concat(arg2))
}
}
}
let test = currie(foo)
// 将之前的多元函数 foo(1,2,3) 转换为了 test(1)(2)(3) 一元函数格式
test(1)(2)(3)
有了柯里化,可以顺利将多元化解为一元,化解为一元后,再通过函数的组合(好比文章开头的例子:xxx.split(’ ‘) .map(v => v.slice(0, 1).toUpperCase() + v.slice(1)).join(’ '))后,完成复杂功能
在Lodash 库中,也有编写好的curry函数,同时还有很多其他优秀的工具函数。对于函数式编程这一思想,只有实际中慢慢体会,多思考别人的写法。