函数式编程的基本思想和概念

1,047 阅读4分钟

为什么学习函数式编程

  • 函数式编程随着react的流行变得越来越受关注; react 和 redux 使用了函数式编程的思想
  • Vue3拥抱了函数式编程
  • 函数式编程可以抛弃this
  • 方便测试 方便并行处理
  • 打包过程中可以更好的利用 tree shaking过滤掉无用的代码

函数式编程的概念 FP

函数式编程是一种编程范式。

面向对象的编程思维方式:把现实世界中的事物抽象成程序世界的类和对象,通过封装、继承和多态来掩饰事物的联系

面向函数式编程的思维方式:把现实世界中的事物和事物之间的联系抽象到程序世界(对运算的过程进行抽象)

  • 程序的本质:根据输入通过某种运算获得相应的输出
  • 函数式编程中的函数指的不是程序中的函数,而是数学中的函数即映射关系
  • 相同的输入始终要得到相同的输出(纯函数)
  • 函数式编程用来描述输入和输出之间的映射 就是对运算过程的抽象

功能: 可以让函数最大程度被重用。

// 非函数式
let num1 = 1;
let nun2 = 2;
let sum = num2+num1

// 函数式编程
function add(n1,n2){
    return n1 + n2
}

函数是一等公民

  • 函数可以存储在编程当中
  • 函数作为参数
  • 函数作为返回值

在JavaScript中函数就是一个普通的对象,我们可以把函数存储在变量/数组当中,还可以作为另外一个函数的返回值

1.函数给变量赋值:

const BC = {
    index(vie){return Views.index(vie)}
}
const Bc = {
    index: Views.index
}

高阶函数

定义:

  • 可以把函数作为参数传递给另外一个函数
  • 可以把函数作为另外一个函数的返回结果

1. 函数作为参数的

好处: 可以让函数变得更灵活

//模拟forEach方法
function forEach(arr,fn) {
    for(let i=0; i<arr.length; i++){
        fn(arr[i])
    }
}

function filter(arr, fn){
    // 存储满足结果的数组
    let res = []
    for(let i=0; i<arr.length; i++){
        if(fn(arr[i])){
            res.push(arr[i])
        }
    }
    return res
}

2.函数作为返回值

函数作为返回值: 就是让一个函数去生成另外一个函数

定义一个函数只被调用一次,利用函数作为返回值和闭包实现

function once(fn){
    let done = false
    return function () {
        if(!done){
            done = true
            return fn.apply(this, arguments)
        }
    }
}

let pay = once(function (params) {
    console.log(`本次账单支付来了${params}元`)
})
pay(5)
pay(6)

使用高阶函数的意义:

  • 可以用来解决抽象通用的问题
  • 抽象可以帮我们屏蔽实现的细节,我们在调用的时候只需要关注我们的目标

闭包

闭包:函数和其周围的状态(词法环境)的引用绑定在一起形成闭包。

可以在一个函数中调用另外一个函数内部的函数,并且可以访问这个函数内部的所有变量成员

闭包的本质: 函数在执行的时候会被放在一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部的成员。

function makePower(power) {
    return function (number) {
        return Math.power(power, number)
    }
}
let pow = makePower(2)
console.log(pow(4))
console.log(pow(5))

纯函数

  • 相同的输入永远会得到相同的输出 而且没有任何副作用
  • lodash是一个纯函数的功能库,提供了对数组、数字、字符串的操作方法
  • 数组中的slice和splice分别是纯函数和不纯函数 返回值一个不会改变原数组一个会改变原数组
  • 函数式编程不会保留中间的计算结果,所以变量是不可变的是无状态的
  • 我们可以把一个函数的执行结果交给另外一个函数去处理
// 纯函数 输入 输出 相同的输入相同的输出
function sum(n1, n2){
    return n1 + n2
}

纯函数的好处

  • 可缓存 因为纯函数的输入和输出始终相同,所以可以把纯函数的结果缓存起来 避免多次执行减少不必要的性能消化
  • 可测试 纯函数让测试更加方便
  • 并行处理: 在多线程的环境下操作共享数据很可能会出现意外,但是纯函数不需要访问共享数据,所以可以在并行环境下任意运纯函数

模拟缓存函数的实现:

function memoize(fn) {
    let cache = {};
    return function (params) {
        if (cache[params]) return cache[params];
        return (cache[params] = fn(params));
    };
}

function getArea(params) {
    console.log("开始执行。。。");
    return params * params;
}

let getAreaWithMemozi = memoize(getArea);
console.log(getAreaWithMemozi(2));
console.log(getAreaWithMemozi(2));
console.log(getAreaWithMemozi(2));

/**
开始执行。。。
4
4
4
*/

副作用函数

副作用:让一个函数变得不纯,函数如果依赖于外部的状态就无法保证输出相同,从而带来副作用。

// 带有副作用的函数 当min发生改变 返回值可能发生改变
let age = 18
let min = 18
function checkAge(age) {
    return age >= min
}

// 改造成纯函数 将最小值放在函数内部
function checkAge(age) {
    let minage = 18 // 硬编码 
    return age >= minage
}

所有的外部交互都有可能产生副作用,副作用使得方法通用性下降不适合拓展和可塑性,同时副作用给程序带来安全隐患和不确定性,副作用不可能完全禁止,尽可能控制在可控范围内发生。

函数柯里化

函数柯里化:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后接受一个新的函数接受剩余的参数,返回结果。

功能: 将一个可以传入n个参数的函数转化成可以传入小于n的参数的函数,直到传入的参数个数和形参个数相同之后执行传入的功能函数。

使用柯里化解决函数中硬编码的问题:


// 普通纯函数
function checkAge(min, age){
    return age >= min
}

// 柯里化 将定值使用闭包的技巧固定下来
function checkAge(min) {
    return function (age) {
        return age >= min;
    };
}
let checkAge18 = checkAge(18);
checkAge18(24);

函数柯里化的原理:

function curry(func) {
    // 首先返回的是一个函数
    return function curriedFn(...args){
        // 先判断传入的实际参数和形参的个数是否相同
        if(args.length <= func.length){
            // 如果小于传入的参数 将上次传入的参数和这次传入的参数做一层合并 递归调用当前函数
            return function(){
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 如果传入的和形参个数相同 执行传入的函数
        return func(...args)
    }
}

柯里化总结

  • 函数柯里化可以让我们对一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的 缓存
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生更强大的功能

函数组合

解决的问题:纯函数和函数柯里化很容易写出洋葱代码

函数组合: 可以让我们把细粒度的函数重新组合生成一个新的函数实现相同的功能

定义: 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

  • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
  • 函数组合默认是从右至左执行的

a7c416c58fc24981791ae0d7b66dfc96.png

function compose(f, g) {
    return function (value) {
        return f(g(value));
    };
}
function reverse(arr) {
    return arr.reverse();
}
function first(arr) {
    return arr[0];
}

let last = compose(first, reverse);
console.log(last([1,2,3]))

函数组合原理:

function compose(...args){
    return function(value){
        return args.reverse().reduce((acc,fn) =>fn(acc),value)
    }
}

reduce方法的原理:对数组中每一个元素执行由我们提供的元素,并将其汇总成一个单个的结果

函数组合的调试: 在每个组合函数之间添加自己写的测试函数。

函子 functor

什么是函子:

  • 容器:包含值和值的变形关系
  • 函子: 一种特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理

作用:将函数式编程中的副作用控制在可控的范围内,异常处理、异步操作

// 对值的处理始终调用内部的map方法
class Container {
    constructor(value) {
        this._value = value;
    }
    map(fn) {
        // 将结果返回一个新的函子对象  始终不将值对外公布
        return new Container(fn(this._value));
    }
}

var c = new Container(5);
let r = c.map((x) => x + 1).map(x => x*x)
console.log(r) // Container { _value: 36 }

// 改造成函数式编程
class Containers {
    static of(value) {
        // 将new操作封装到静态方法中
        return new Containers(value);
    }

    constructor(value) {
        this._value = value;
    }
    map(fn) {
        // 将结果返回一个新的函子对象  始终不将值对外公布
        return Containers.of(fn(this._value) );
    }
}

let res = new Container(5).map((x) => x + 1).map((x) => x * x);
console.log(res)

总结:

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了map契约的对象
  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含新值的盒子(函子)