函数式编程基础

196 阅读7分钟

概念:把现实世界的事物和事件之间的关系抽象到程序中来,对运算过程进行抽象。函数是指的是数学中的映射关系,相同的输入始终要得到相同的输出结果。 优点:可以抛弃掉烦人的 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))。函数组合可以把细粒度的代码组合成一个新函数。

管道

image.png 可以想象 数据 a 通过管道 生成数据 b,当 fun1 函数比较复杂时,我们可以将这个管道拆分成多个管道。 image.png 将 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 模快提供了对函数的友好的方法,函数(只传一个参数)优先数据滞后,其内都是通过柯里化处理的函数,就不需要我们去手动的去柯里化了。

总结

函数式编程是一种编程思想,当然这是附加技能栈,根据自己的兴趣去学习 ,需要循序渐进的学习和使用,不可囫囵吞枣,其大致也就是把运算过程抽象成细小的函数组合成大功能的函数,可以使用到柯里化,和组合等。

相关参考

掘金:juejin.cn/post/684490…