1. 什么是函数式编程
最开始出现的是命令式编程,就是面向过程的编程,把问题按照步骤一步一步来执行,强调的是怎么去做。这样会产生很多临时变量,而且代码复用率不高
随后就出现了面向对象编程和函数式编程,面向对象编程这就不过多介绍,而函数式编程它着眼点在函数,而不是过程,它强调的是需要去做什么,如何通过函数的组合变换去解决问题
函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一个函数的输出类型可以匹配另一个函数的输入,所以在编程过程中要关注如何去构建关系
2. 函数式编程的特点
2.1 函数是“一等公民”
函数是“一等公民”这是函数式编程得以实现的前提,因为基本的操作都是在操作函数,这就意味着函数与其他数据类型一样处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值
2.2 声明式编程
函数式编程大多数是在声明需要做什么,而不是怎么去做,这样的编程风格就称为声明式编程,它描述了一系列的操作,但不会暴露它们是如何实现的或数据流如何传过它们
好处就是代码可读性高,不用关心具体的实现,方便分工协作
我们熟知的 SQL 语句就是很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象
2.3 惰性执行
惰性执行就是函数只在需要的时候才执行,即不产生中间变量
函数式编程和命令式编程区别就在于几乎没有产生中间变量,从头到尾都在写函数,只在最后的时候才调用
2.4 无状态和数据不可变
这是函数式编程的核心概念:
数据不可变:要求所有的数据都是不可变的,意味着如果想要修改一个对象就应该创建一个新的对象来修改,而不是修改已有的对象
无状态(引用透明):给定相同的输入,给出相同的输出,不依赖外部状态的变化
为了实现这个目标,函数式编程提出函数应该具备的特性:没有副作用和纯函数
2.5 没有副作用
副作用:在完成函数主要功能之外完成的其他副要功能
在函数中最主要的功能就是根据输入返回结果,最常见的副作用就是随意修改外部变量
保证函数没有副作用:
- 保证数据的不可变性
- 避免因为共享状态而带来的问题
2.6 纯函数
纯函数算是“没有副作用”的要求上更进一步,它就是简单的两点:
-
不依赖外部状态(无状态):函数的运行结果不依赖全局变量、this 指针、IO 操作等
-
没有副作用(数据不变):不修改全局变量,不修改入参
纯函数就是真正意义上的“函数”,意味着相同的输入,永远会得到相同的输出
纯函数的好处:
- 便于测试和优化,符合测试驱动开发 TDD 的思想
- 可缓存性:因为相同的输入总是会返回相同的输出,我们就可以提前缓存函数的执行结果
- 更少的 bug
3. 常见函数式概念
3.1 柯里化 - currying
柯里化就是将使用多个参数的一个函数拆分成一系列使用一个参数的函数 (将多元函数转换成一元函数)
函数表达:f(a, b, c) => f (a) (b) (c)
//原始的加法函数add
function add(x, y, z) {
return x + y + z;
}
// 改成柯里化函数
function curriedAdd(x) {
return function(y) {
return function(z) {
return x + y + z;
};
};
}
curriedAdd(1)(2)(3) // 6
手写实现柯里化:
// 定义柯里化函数
const curry = (fn) => {
return function curryFunc(...arg) {
if (arg.length < fn.length) {
return function () {
return curryFunc.apply(null, [...arg, ...arguments]);
};
}
return fn.apply(null, arg);
}
};
const func = (a, b) => console.log(a - b);
curry(func)(1)(2)
3.2 组合 - compose
组合 compose 指的是将多个函数组合成一个函数,这样一个函数的输出就可以作为另一个函数的输入,从而实现多个函数的链式调用
函数表达:compose(f, g, t) => x => f(g(t(x))
,进一步结合 curry 可以实现 compose(f)(g)(t) => x => f(g(t(x))
,执行顺序式从右至左执行的
3.3 管道 - pipe
管道 pipe 函数是一个高阶函数,它接受一系列函数作为参数,将函数串联起来,一步步将上一步的输出作为下一步的输入
函数表达:pipe(f, g, t) => x => t(g(f(x))
,进一步结合 curry 可以实现 pipe(f)(g)(t) => x => t(g(f(x))
,执行顺序式从左至右执行的
组合和管道功能是一样的,本质没什么区别,只是执行顺序不一样