概述

函数式编程比较复杂比较枯燥,但是为了了解react和redux,如果没有函数式编程的理论铺垫,很难学好他们。

函数式编程在js当中是一个比较抽象的概念,大家在以前可能听说过函数式编程,但是可能并没有系统的去了解过他们。

函数式编程和面向对象编程一样,是一套编程范式,你可以根据函数式编程的理论为你的代码设计这个过程。只不过但是函数式编程要求相对比较高一些

为什么要去学习函数式编程

函数式编程其实相对于计算机的历史而言是一个非常古老的概念,甚至早已第一台计算机的诞生。他的演算并非设计在计算机上执行,而是在20世纪三十年代引入的一套用于研究函数定义,函数应用和递归的形式系统。

也就是说函数式编程已经是一个很老的概念了,那为什么我们还要学习他,其实函数式编程以前和前端没有任何关系,也并不流行。只是因为react和redux将它带火了。有了高阶函数,那么高阶函数就是函数式编程的一部分,所以才将函数式编程带火了。

函数式编程主要是用于研究函数的定义,函数的应用和递归的而这样一个形式的系统。

注意,函数式编程不是用函数来编程,也不是传统的面向过程编程,主旨在于将复杂的函数复合成简单的函数,运算过程尽量写成一系列嵌套的函数调用。大家注意区分用函数编程和函数式编程是不同的。

react的高阶组件,使用了高阶函数来实现,高阶函数就是函数式编程的一个特性,我们后面会学到。虽然react当中使用了一些函数式编程的特性,但它并不是纯函数式的。

另外react的一些生态,比如redux,它使用了函数式编程的一些思想,所以我们想要更好的学习react和redux的话,我们需要了解函数式编程。

我们都知道vue3对vue2做了很大的重构,而且越来越偏向函数式,我们在使用vue3的一些api的时候可以感受到,在vue2的源码中也大量的时候到了高阶函数,这些流行框架都在趋向于函数式编程,我们甚至可以说你可以不学习这些框架,但是你不能不了解函数式编程。因为这些才是永远不变的内容。

很多同学再学习js之前可能都了解过面向对象的语言,比如说Java,C#以及C++等等,所以在学习js的时候,我们也都是从面向对象开始学习的,我们会通过学习原型,原型链以及模拟实现继承的机制来实现面向对象的一些特性。我们在学习的过程中还会遇到this的各种各样问题,如果我们用到函数式编程的时候,我们就可以抛弃掉this。

是用函数式编程有很多的好处,比如说打包的时候可 以更好的利用tree-shaking来过滤无用的代码。

使用函数式编程还可以方便测试,方便并行处理,这些都是由函数式编程的特性来决定的。

还有很多库可以帮助我们进行函数式开发,比如说lodash,underscore,ramda。

这就是为什么要学习函数式编程。

函数式编程的概念

函数式编程是范畴轮的数学分支,是一门很复杂的数学,认为世界上所有的概念体系都可以抽象出一个范畴。范畴可以理解为群体的概念,比如一个班级中的同学,就可以理解为一个范畴。

只要彼此之间存在某种关系概念,事物,对象等等,都构成范畴,任何事物只要找出他们之间的关系,就可以被定义。比如说教室中上课的人,可以彼此都不认识,但是大家的关系是同学,所以就是一个范畴。

关系一般用箭头来表示,正式的名称叫做 态射 。范畴轮认为,同一个范畴的所有成员,就是不同状态的变形。通过态射一个成员就可以变形成另一个成员。简单来说就是每个成员之间都是有关系的。

函数式编程英文的叫法是Functional Programming 缩写是FP。函数式编程是一种编程范式,我们可以认为他是一种编程的风格,他和面向对象是并列的关系。函数式编程我们可以认为是一种思维的模式,我们常听说的编程范式,还有面向过程变成和面向对象编程。

函数式编程的思维方式,是把现实世界中的事物,和事物之间的联系,抽象到程序世界中。

我们首先来解释一下程序的本质,就是根据输入然后根据某种运算获得相应的输出,程序在开发的过程中会涉及到很多说如和输出的函数,函数式编程就是对这些运算过程进行抽象。

假设我们有个输入x,可以通过一个映射关系变成y,那这个映射关系就是函数式编程中的函数。

关于函数式编程我们要注意的是,函数式编程中的函数,不是程序中的函数或者方法,不是说我们在编程过程中使用到了函数或者方法就是函数式编程,函数式编程中的函数,值得其实是数学中的函数,数学中的函数是用来描述映射关系的,例如 y = sin(x) 这个函数,sin是用来描述x和y的关系。当x=1时y的值也就确定了,也就是说当x的值确定了y的值一定也是确定的。

在函数式编程中我们要求相同的输入始终要得到相同的输出,这是纯函数的概念。

函数式编程就是对运算过程的抽象,下面我们用一段代码来体会一下函数式编程。

比如我们要计算两个数的和,并且打印这个结果,我们一般会定义两个变量num1和num2,然后将这个两个变量想加,最后打印想加的结果。

let num1 = 2;
let num2 = 3;
let num = num1 + num2;
console.log(sum)

那这是非函数式的,如果使用函数式的思想应该像下面这样,首先我们要对运算过程抽象add函数,这个函数接收两个参数n1和n2,当这个函数执行之后会把结果返回。也就是说,函数式编程中的函数一定要有输入和输出。

function add (n1, n2) {
    return n1 + n2;
}
let sum = add(2, 3);
console.log(sum);

可以看到,当我们使用函数式编程的时候一定会有一些函数,这些函数后续可以无数次的重用,所以函数式编程的一个好处就是,可以让代码进行重用,而且在函数式编程的过程中,抽象出来的函数都是细粒度的,那这些函数将来可以重新去组合成功能更强大的函数。

函数式编程不是说写几个函数就是函数式开发,他是用数学的思维方式借助js的语法来进行一些代码的开发,所以说函数式他是一套数学的规律。

那这样他跟我们平常写代码有什么区别呢?用函数式编程的时候我们是不可以用if的,也没有else,因为数学中不存在if和else,也没有变量和while,整个都是数学的思维,然后用js的语法来承接。可以使用递归,因为递归是数学的概念。

函数是一等公民

所谓一等公民,指的是函数与其它数据类型一样,处于平等地位,可以赋值给其它变量,可以作为参数,也可以作为返回值。

在函数式编程中,变量是不能被修改的,所有的变量只能被赋值一次,所有的值全都靠传参来解决。

所以简单来说就是,函数是一等公民,可以赋值给变量,可以当做参数传递,可以作为返回值。

在函数式编程中,只能用表达式,不能用语句,因为数学里面没有语句。

因为变量只能被赋值一次,不能修改变量的值,所以不存在副作用,也不能修改状态。

函数之间运行全靠参数传递,而且这个参数是不会被修改的,所以引用比较透明。

高阶函数

高阶函数的定义其实很简单,就是如果一个函数A可以接收另一个函数B作为参数,那么这种函数A就称之为高阶函数。说简单一点就是参数列表中包含函数。

函数式编程的思想是对运算过程进行抽象,也就是把运算过程抽象成函数,然后在任何地方都可以去重用这些函数。

抽象可以帮我们屏蔽实现的细节,我们以后在调用这些函数的时候只需要关注我们的目标就可以了,那高阶函数就是帮我们抽象这些通用的问题。

我们举一个简单的例子,比如说我们想遍历打印数组中的每一个元素,如果我们使用面向过程编程代码如下。

可以发现我们要写一个循环来做这样一件事,我们要关注数组的长度,要控制变量不能大于数组长度,要关心很多东西。

// 面向过程方式
let array = [1, 2, 3, 4];
for (let i = 0; i < array.length; i++) {
    console.log(array[i]);
}

我们这里Array的forEach函数实现,我们在使用的时候不需要关注循环的具体实现,也就是不需要关注循环的细节,也不需要变量去控制,我们只需要知道forEach函数可以帮我们完成循环就ok了。

// 高阶函数
let array = [1, 2, 3, 4];
array.forEach(item => {
    console.log(item);
})

这里的forEach就是对通用问题的一个抽象,我们可以看到使用forEach要比for循环简洁很多,所以我们使用函数式编程还有一个好处就是使代码更简洁。

在js中,数组的forEach,map,filter,every,some,find, findIndex, reduce, sort等都是高阶函数,因为他们都可以接收一个函数为参数。

闭包

函数和其周围的状态的引用捆绑在一起,可以在另一个作用域中调用这个函数内部的函数并访问到该函数作用域中的成员。

闭包的概念并不复杂,但是他的定义比较绕,我们通过一段代码来体会闭包的概念。

首先我们定义一个makeFn的函数,在这个函数中定义一个变量msg,当这个函数调用之后,msg就会被释放掉。

function makeFn () {
    let msg = 'Hello';
}

maknFn();

如果我们在makeFn中返回一个函数,在这个函数中又访问了msg,那这就是闭包。

和刚刚不一样的是,当我们调用完makeFn之后他会返回一个函数,接收的fn其实就是接收makeFn返回的函数,也就意味着外部的fn对函数内部的msg存在引用。

所以当我们调用fn的时候,也就是调用了内部函数,会访问到msg,也就是makeFn中的变量。

function makeFn () {
    let msg = 'Hello';
    return function() {
        console.log(msg);
    }
}

const fn = maknFn();

fn();

所以闭包就是在另一个作用域(这里是全局),可以调用到一个函数内部的函数(makeFn内部返回的函数),在这个函数中可以访问到这个函数(makeFn)作用域中的成员。

根据上面的描述,闭包的核心作用就是把我们makeFn中内部成员的作用范围延长了,正常情况下makeFn执行完毕之后msg会被释放掉,但是这里因为外部还在继续引用msg,所以并没有被释放。

我们接下来看下下面这个例子, 介绍一下闭包的作用。

这里有一个once函数,他的作用就是控制fn函数只会执行一次,那如何控制fn只能执行一次呢?这里就需要有一个标记来记录,这个函数是否被执行了,我们这里定义一个局部变量done,默认情况下是false,也就是fn并没有被执行。

在once函数内部返回了一个函数,在这个新返回的函数内部先去判断done,如果done为false,就把他标记为true,并且返回fn的调用。

当once被执行的时候,我们创建一个done,并且返回一个函数。这个函数我们赋值给pay。

当我们调用pay的时候,会访问到外部的done,判断done是否为false,如果是将done修改为true,并且执行fn。这样在下一次次调用pay的时候,由于done已经为true了,所以就不会再次执行了。

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

let pay = once(function(money) {
    console.log(`${money}`);
});

// 只会执行一次。
pay(1);
pay(2);

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