概念:把现实世界的事物和事件之间的关系抽象到程序中来,对运算过程进行抽象。函数是指的是数学中的映射关系,相同的输入始终要得到相同的输出结果。 优点:可以抛弃掉烦人的 this ,在打包的时候可以进行 tree-shaking 过滤无用代码,方便测试,方便并行处理。
小栗子
//非函数式
let num1 = 10;
let num2 = 10
let res = num1 + num2;
console.log(res)
//函数式
let add = function(b,c){
return b + c
}
console.log(add(10,10))
函数
函数式一等公民,先来看下函数在 MDN 的简介,因为函数是一个普通的对象。
- 可以传递参数
- 可以作为返回值
- 可以作为变量存储
这样,就可以将函数或者方法赋值给另一个函数或者方法了,有了这几个特性为函数值变成提供了有利的条件
高阶函数
high-orider function
函数作为参数。
看看如何使用高阶函数来模拟数组的一些方法,顺便练习下,加深这些方法的使用。
// 实现 forEach
function myForEach(arr, fun) {
for (let i = 0; i < arr.length; i++) {
fun(arr[i])
}
}
myForEach(arr, (item) => {
console.log(item)
})
//实现reduce
function myReduce(arr, fun, init) {
let res = arr[0],a = 1;
// 有初始值
if(init){
res = init;
a = 0;
}
for (let i = a; i < arr.length; i++) {
res = fun(res,arr[i],i)
}
return res;
}
let num = myReduce(arr, (total,current,index) => {
return total + current
},10)
console.log(num)
//实现filter
function myFilter(arr, fun) {
let res = []
for (let i = 0 ;i < arr.length; i++) {
let resC = fun(arr[i])
if(resC){
res.push(resC)
}
}
return res;
}
let num = myFilter(arr, (item) => {
if(item >= 2 ){
return item
}
})
console.log(num)
通过以上三个小例,可以了解到函数作为参数的应用,
函数作为返回值
function fun() {
console.log(this.name)
}
let obj = {
name:'obj'
}
let funToObj = fun.bind(obj)
funToObj()
// fun.bind(obj)() //或者立即调用
function once(fun) {
let done = false;
let self = this;
return function () {
if (!done) {
done = true
fun.apply(self, [...arguments])
}
}
}
let funOnce = once(function fun(item) {
console.log(item)
})
funOnce(2)
funOnce(2)
通过函数生成了一个函数,通过创建闭包记录这个函数的执行记录,来实现 once 的效果。
总结
通过以上 myForEach 等示例可以看到,我们封装了函数遍历的细节,在调用时只去关心我们想要的目标,以此来将结果一类问题的解决过程抽象成一个函数,这有点类似于面相对象编程的封装。
纯函数
相同的输入永远会得到相同的输出结果,而且没有任何可观察的副作用,就好比身份证永远只会对应一个特定的人一样,输入这个身份证返回来的就是这个结果用远不会变(你要是杠我那可就没办法了)。
什么是纯函数
一个栗子:大家都用过数组的 splice 和 slice 方法吧,前者会改变原数组,并返切下来的数组,后者则只是截取,对原数组无任何影响,结合纯函数的定义,相同的输入一定会有相同的输出,很明显,前者不是,第二次的输入肯定不和第一次的相等(两次都切割值,其数组不重复),所有,spclie 就是非纯函数,slice 为纯函数。
但严谨的来说,这两个都不是纯函数,因为纯函数是需要参数的。
//函数式
let add = function(b,c){
return b + c
}
console.log(add(10,10))
就比如这个 add(10,10)方法,不管输入多少次他永远都等于 20,所以我们可以编写细粒度的函数最去组合为功能强大函数。
纯函数的好处
- 可缓存:因为参数相同会返回相同的结果,所以通过一次计算缓存计算复杂耗时的计算结果。
- 可测试:让测试更方便,如断言测试,断言他的结果(纯函数必有输入输出)
- 并行处理:多线程操作共享变量时,将会有意外的情况,而纯函数只依赖参数,且不需要共享内存数据,所以可以在任意环境使用纯函数。
副作用
如果一个函数的返回依赖外部状态,则无法保证相同的输出,则会产生副作用。 副作用的来源有 配置文件、数据库、用户输入等。所以,所有的外部交互都会产生副作用,使得方法的通用性下降,不适合扩展和重用。也可能带来安全隐患。
柯里化
先传递一部分参数,再将以此参数为基准的函数返回成一个新函数去接受剩余的变量。
//硬编码导致函数不纯
function moreThan_10(b) {
let _num = 10;
return b > _num ? true : false
}
//柯里化解决
function moreThan(a) {
return function (b) {
return b > a ? true : false
}
}
let FunMoreThan_10 = moreThan(10)//动态的去确定他的基准
console.log(moreThan_10(2))
console.log(FunMoreThan_10(2))//新函数去接受剩余的参数
解决了因硬编码导致了函数不纯的问题。
通用方法
功能描述:如果需要的参数都被满足则执行,否则返回该函数继续等待剩余参数。
- 也就是说当给一个函数传入较少的参数时,让他返回记住这些参数的新函数。
- 对函数的一种缓存。
- 让函数更灵活,粒度更小。
- 可以把多元函数转换为一元函数,组合使用产生更强大的功能。
//硬编码导致函数不纯
function _curry(fun) {
return function creater(...args){
if(args.length < fun.length){
return function(){
return creater(...args.concat(Array.from(arguments)))
}
}
return fun(...args)
}
}
function _curry(fun) {
let params = []
let needLen = fun.length;
let addFun = function () {
params.push(...arguments)
console.log(params)
if (needLen === params.length) {
return fun.apply(fun, params.splice(0))
} else {
return addFun
}
}
return addFun;
}
//可以分开调用 但是有漏洞
函数组合
纯函数和科里化容易产生洋葱代码,如 a(b(c(parmas))。函数组合可以把细粒度的代码组合成一个新函数。
管道
可以想象 数据 a 通过管道 生成数据 b,当 fun1 函数比较复杂时,我们可以将这个管道拆分成多个管道。
将 fun1 这个大管道拆分成小的管道,此时,中间的运算过程产生了 a-1 和 a-2 参数,也成为了中间结果,在组合的时候是不需要考虑中间结果的。所以,需要多个函数去处理一个值得时候,可以将中间这些函数组合成一个函数,就像是将这些小管道连接形成大管道,让参数通过大管道得到结果,且组合的时候默认从右到左。
let fun1 = compose(fun_1, fun_2, fun_3)
要注意,函数组合的是能是纯函数,模拟下 flowRight 函数组合的方法。
// 定义的纯函数
const fist = arr => arr[0]
const reverse = arr => arr.reverse()
const toUpper = s => s.toUpperCase()
// 函数组合
function flow(...funArgs) {
return function (params) {
// 对数组用循环的时候,我们首先考虑下使用 reduce
// while (funArgs.length) {
// let fun = funArgs.shift()
// res = fun(res)
// }
return funArgs.reduce((lastRes,current)=>{
return current(lastRes)
},params)
}
}
// 生成新函数
let fun1 = flow(reverse, fist, toUpper)
console.log(fun1(['1', 2, 3, '4']))
箭头函数写法
const flow = (...args) => (value) => args.reduce((lastRes, cFun) => cFun(lastRes), value)
为什么要用箭头函数,因为懒,怕多打几个字符(狗头保命)
结合律
这也不难解释,就是咱数学中的结合律一样,意指在一个包含有二个以上的可结合运算子的表示式,只要算子的位置没有改变,其运算的顺序就不会对运算出来的值有影响一个栗子明白。
let fun0 = flow(reverse, fist, toUpper)
let fun1 = flow(flowRight(reverse, fist), toUpper)
let fun2 = flow(reverse, flowRight(fist, toUpper))
console.log(fun0(['1', 2, 3, '4']))
console.log(fun1(['1', 2, 3, '4']))
console.log(fun2(['1', 2, 3, '4']))
// 不管怎样结合,打印的效果都是一样的
调试
那这样拼接一个大管道,如果出错,怎样才能管道的那个部位出错了呢,相信很聪明的你已经想到了,在管道的每个节点添加打印函数,并将值返回,就可以监测管道通过的数据了,为了更好监测管道的数据,尝试给每个数据前添加标签,这就用到了柯里化,相信,前面的讲解和下面的完整例子你肯定就懂了。
// 定义的纯函数
const fist = arr => arr[0]
const reverse = arr => arr.reverse()
const toUpper = s => s.toUpperCase()
// 柯里化
function _curry(fun) {
return function addFun(...args) {
if (fun.length > args.length) {
return function () {
return addFun(...args.concat(Array.from(arguments)))
}
}
return fun(...args)
}
}
// 纯函数组合
const flow = (...args) => (value) => args.reduce((lastRes, cFun) => cFun(lastRes), value)
// 监测管道数组 tag 添加标签 ,value 管道的输出值
const trace = _curry((tag, value) => {
console.log(tag, value)
return value;
})
//trace('reverse:') 设置标签 ‘reverse:’后记忆,后等待管道的 value 输入
// pointfree 模式,指明一系列运算,未定义数据
let fun = flow(reverse, trace('reverse:'), fist, trace('fist:'), toUpper)
console.log(fun(['1', '2', '3', '4']))
lodash 的 fp 模快提供了对函数的友好的方法,函数(只传一个参数)优先数据滞后,其内都是通过柯里化处理的函数,就不需要我们去手动的去柯里化了。
总结
函数式编程是一种编程思想,当然这是附加技能栈,根据自己的兴趣去学习 ,需要循序渐进的学习和使用,不可囫囵吞枣,其大致也就是把运算过程抽象成细小的函数组合成大功能的函数,可以使用到柯里化,和组合等。