什么是函数式编程
函数式编程(Function Programming,FP),FP 是编程规范之一,我们常听说的编程规范还有面向过程编程、面向对象编程。
- 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。
- 函数式编程的思维方式是:把现实事件的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
- x -> f(联系、映射)->y,y = f(x)
- 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x 和 y 的关系
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射
高阶函数
高阶函数(Higher-order Function)
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回值
函数作为参数
实现forEachconst forEach = function (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i]);
}
}
// 测试
const arr = [1, 2, 3, 4];
const fn = function (item) {
console.log(item);
}
forEach(arr, fn);
函数作为返回值 - 实现once
实现lodash的once方法。once 函数传入函数参数只执行一次。 ```javascript const once = function (fn) { let done = false; return function () {
} } const zf = function (money) { console.log(if (!done) {
done = true;
fn.apply(null, arguments);
}
支付了${money}元
) } const pay = once(zf);
// 只会支付一次 pay(1) pay(2) pay(4)
<a name="ovGiw"></a>
# 闭包
<a name="ukOIO"></a>
# 纯函数的好处 及 副作用
纯函数指:对于相同的输入永远会得到相同的输出,而且没有任何可观察的**副作用**。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/21370038/1646247933067-3bb1a965-574c-48fd-b910-c4b06c245484.png#clientId=u33521a4b-893c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=192&id=u7ca44b7d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=384&originWidth=508&originalType=binary&ratio=1&rotation=0&showTitle=false&size=102458&status=done&style=none&taskId=u9f73ef0d-d704-4e1e-a24d-cb4129e0044&title=&width=254)<br />副作用让一个函数变的不纯(如上例),纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。<br />**副作用的来源**有:配置文件、数据库、获取用户的输入、......<br />所有的外部交互都有可能产生副作用,副作用也使得方法通用性下降不利于拓展和可重用性,同时副作用会给程序中带来安全隐患和不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
纯函数的代表 -> [Lodash 源码](https://github.com/lodash/lodash)
<a name="JBeE4"></a>
## 可缓存 - 实现memoize
因为纯函数对相同对输入始终有相同对结果,所以可以把纯函数对结果缓存起来。<br />_.memoize(func, [resolver])
> 创建一个会缓存 func 结果的函数。 如果提供了 resolver ,就用 resolver 的返回值作为 key 缓存函数的结果。 默认情况下用第一个参数作为缓存的 key。 func 在调用时 this 会绑定在缓存函数上。
```javascript
// 记忆函数
const _ = require('lodash');
// 计算圆面积
function getArea(r) {
console.log(r);
return Math.PI * r * r
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
// 4
// 50.2654824....
// 50.2654824....
// 50.2654824....
只输出了一次 4 所以getArea只被执行了一次 后续的面积是从缓存中拿的
模拟 memoize 方法的实现
- memoize 接受函数作为参数,这个函数是纯函数
- 内部返回一个函数
- 函数中需要把fn指向的结果缓存起来:
- memoize 中定义一个对象,把执行结果缓存起来。之后再调用该函数时从缓存中获取。
- 因为 memoize 的入参函数是纯函数(相同的输入有相同的输出),所以我们缓存对象的键为纯函数的输入,值为纯函数的输出
function memoize(fn) {
const cache = {}
return function () {
// arguments 是伪数组,转字符串作为 cache 键
let key = JSON.stringify(arguments);
cache[key] = cache[key] || fn.apply(null, arguments);
return cache[key];
}
}
// 测试
function getArea(r) {
console.log(r);
return Math.PI * r * r
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
可测试
纯函数让测试更加方便
并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况(多个线程同时修改一个全局变量,此时该变量结果是什么就无法预知)。
- 纯函数不需要访问共享的内存数据(纯函数是封闭的空间,只依赖于参数,不需要访问共享内存),所以在并行环境下可以任意运行纯函数(Web Worker)
柯里化
是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术
lodash中的柯里化 - 实现curry
_.curry(func)
- 功能:创建一个函数,该函数接受一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
// 自定义的柯里化方法
const curry = function (func) {
return function curriedFn(...args) {
// 判断实参个数和形参个数
// args.length为柯里化后函数的参数(实参)
// func.length为需要柯里化函数的参数个数(形参)
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)));
}
}
return func(...args);
}
}
// 测试
// 需要颗粒化的函数
function getSum(a, b, c) {
return a + b + c;
}
// 柯里化后的函数
const curried = curry(getSum);
console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
console.log(curried(1)(2)(3))
总结:
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数。
- 这是一种对函数参数的’缓存’。
- 让函数更加灵活,让函数的粒度更小。
- 可以把多元函数换成一元函数,可以组合使用函数产生强大的功能。
函数组合
纯函数和柯里化很容易写出洋葱代码 h( g( f( x ) ) ) ,如:获取数组最后一个元素再转换成大写字母:_.toUpper( _.first( _.reverse( array ) ) )
函数组合⬇️可以让我们把细粒度的函数重新组合生成一个新的函数。
管道
// 组合函数伪代码
fn = compose(f1, f2, f3);
b = fn(a);
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数,处理过中这些中间函数会得到相应的中间结果,这些中间结果我们不需要关注。
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。
- 函数组合默认从右到左执行。
// 函数组合的演示,实现求数组最后一位
function compose(f1, f2) {
return function (value) {
return f1(f2(value));
}
}
// 返回反转后的数组
function reverse(array) {
return array.reverse();
}
// 返回数组第一个元素
function first(array) {
return array[0];
}
// 组合成可以获取数组最后一位的新函数
const getLast = compose(first, reverse);
console.log(getLast([1, 2, 3])) // 3
实现flowRight
// 自定义的 compose 方法
const compose = function (...args) {
return function (value) {
return args.reverse().reduce((acc, fn) => fn(acc), value);
}
}
// 测试
// 返回反转后的数组
function reverse(array) {
return array.reverse();
}
// 返回数组第一个元素
function first(array) {
return array[0];
}
// 返回大写后的值
function toUpper(s) {
return s.toUpperCase();
}
// 组合成可以获取数组最后一位的新函数
const getLastUpper = compose(toUpper, first, reverse);
console.log(getLastUpper(['abc', 'def', 'ghi']));
函数的组合要满足结合律
我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
const _ = require('lodash');
const f = _.flowRight(_.toUpper, _.first, _.reverse);
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse);
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse));
console.log(f(['abc', 'def', 'ghi']));
Functor 函子
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
函子是一个普通对象,这个对象里维护一个值,并且对外公布一个 map 方法,因此我们可以通过一个类描述函子
class Container {
constructor(value) {
// 创建函子的时候传递一个值,这个值是函子内部维护的不对外公布,约定_开头的成员为私有成员。
this._value = value;
}
// 对外公布一个 map 方法,作用是接受一个处理值的函数,调用map方法时会调用fn去处理这个值,并把处理的结果传递给新的函子由新的函子保存
map(fn) {
return new Container(fn(this._value));
}
}
let r = new Container(5).map(x => x + 1).map(x => x * x)
console.log(r); // Container {_value: 36}
Pointed 函子
pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)
class Container {
static of(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.of(fn(this._value));
}
}
let r = Container.of(5).map(x => x + 2).map(x => x * x);
console.log(r); // Container {_value: 49}
问题:
let r = Container.of(null).map(x => x.toUpperCase());
MayBe 函子
我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe {
static of(value) {
return new MayBe(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
}
isNothing() {
return this._value === null || this._value === undefined;
}
}
let r = MayBe.of(null).map(x => x.toUpperCase());
console.log(r); // MayBe {_value: null}
问题:
let r = MayBe.of('hello world').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '));
console.log(r) // MayBe {_value: null}
虽然我们可以处理空值的问题不会出现异常,但是不知道什么位置出现了空值(异常)。
传入 null 的时候,不会处理map的函数fn,而是仅仅返回一个值为 null 的函子,不会给出任何有效的信息,不会告诉我们是哪块出了问题,出了什么问题。
Either 函子
class Left {
static of(value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this;
}
}
class Right {
static of(value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Right.of(fn(this._value));
}
}
let r1 = Left.of(12).map(x => x - 2);
let r2 = Right.of(12).map(x => x - 2);
console.log(r1) // Left { _value: 12 }
console.log(r2) // Right { _value: 10 }
function toUpperCase(str) {
try {
return Right.of(str.toUpperCase());
} catch (e) {
return Left.of({ error: e.message });
}
}
let R1 = toUpperCase(null);
let R2 = toUpperCase('hello world');
console.log(R1) // Left { _value: { error: "Cannot read property 'toUpperCase' of null" } }
console.log(R2) // Right { _value: 'HELLO WORLD' }
IO 函子
IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
Monad 函子
Monad 函子是可以变扁(解决函子嵌套的问题)的 Pointed 函子,IO(IO(x))
一个函子如果具有 join 和 of 两个方法并且遵守一些定律就是一个 Monad 函子