1. 函数式编程
- 为什么要了解函数式编程?
- react组件使用高阶组件,高阶组件用的是函数式编程
- vue3越来越偏向与函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的过滤无用代码
- 方便测试,方便并行处理
- 有很多库可以帮助进行函数式开发:lodash,underscore, ramda
- 什么是函数式编程?
- 函数式编程是编程范式之一,其他编程范式有: 面向过程编程(按照步骤来实现,一步一步实现想要的功能)、面向对象编程(把现实世界中的事务抽象为程序世界中的类和对象,通过封装、继承、多态来演示事物之间的联系)
- 函数式编程思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 函数式编程中的函数指的不是程序中的函数(方法),而是数据中的函数,要保证相同的输入始终要得到相同的输出;函数式编程用来描述数据(函数)之间的映射。
- 函数式编程是对运算过程进行抽象,在任何时候都可以调用函数,函数可以进行代码复用。
- 函数式编程中的变量是不可变的
2. 函数概念
在JavaScript中,函数就是普通的对象
-
函数是一等公民
- 函数可以存储在变量中
- 函数作为参数
- 函数作为返回值 -
高阶函数
- 函数可以作为另一个函数的入参 (函数作为入参可以使函数变得更灵活)
- 函数可以为另一个函数的返回值 -
高阶函数的意义
- 屏蔽细节,只关注目标;用来抽象通用的问题
- 可以代码更加灵活,例如使用forEach和filter时,不需要关注循环,关注点落在实现的目标上面
// 高阶函数 函数可以作为返回值
// once
function once(fn){
let done = false;
return function() {
if (!done) {
done = true;
}
return fn.apply(this, arguments) // arguments是调用返回值函数时传递的参数
}
}
let pay = once(function (money) {
console.log(money);
} )
pay(5);
pay(5);
// 对console.log(money) 仅执行一次
// every
const every = (array, fn) => {
let result = true; //遍历array,如果有一个不符合fn,则result为false,跳出循环break
for (let value of array) {
// let ... of ... 是 for循环的抽象式写法
result = fn(value);
if (!result) {
break;
}
}
return result;
}
3. 闭包
- 子函数和子函数在父函数的变量环境称为闭包;在闭包环境下,会把值存入内存。
- 在JavaScript中,函数具有对在相同作用域以及任何外部作用域中声明的所有变量的引用,这些作用域被称为函数的词法环境。函数与作用域的组合称为闭包。
- 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
- 可以在另一个作用域中调用一个函数的内部函数并且该内部函数可访问到该函数的作用域中的成员
- 函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除(正常情况下函数内部作用域成员会被释放),但是闭包情况下因为 堆上的作用域成员因为被外部引用不能释放,因为内部函数依旧可以访问该函数的成员
// 闭包案例
function fun (base) {
return function (performance) {
return base + performance;
}
}
let fun_1 = fun(1);
let fun_2 = fun(2);
fun_1(4); // 5
fun_2(4); // 6
4. 纯函数
- 相同的输入始终得到相同的输出。
- 数组中的 slice 返回数组中的指定部分,不会改变原数组(纯函数);splice 对数组进行操作返回该数组,会改变原数组(不纯函数)。
- 可将纯函数的结果进行缓存。(使用lodash库中的记忆函数memoize() )\
- 可测试 (纯函数让测试更方便)
- 并行处理
function memoize(f) {
let cache = {};
return function() {
// 获取到函数f中的参数
let key = JSON.stringify(arguments); // arguments是伪数组
cache[key] = cache[key] || f.apply(f, arguments); // f.apply() 可以改变函数内部的this,第二个参数可以把伪数组或者数组展开
// call apply 区别:call和apply都会立即调用函数,但是call接受逗号分隔的参数列表;apply接受一个参数数组; bind不会立即调用函数,而是返回一个新的函数,这个新函数的this值被永久绑定到指定的对象
return cache[key];
}
}
function getArea(r) {
return Math.PI * r * r;
}
let getMemoize = memoize(getArea);
console.log(getMemoize(4));
造成纯函数变得不存的副作用来源有: 配置文件,全局变量,数据库,获取用户的输入;所有的外部交互都有可能代理副作用,但是副作用不能完全禁止,只能尽可能控制在可控范围内
5. lodash (函数式编程代表库)
- 纯函数的代表库
- lodash中的柯里化:
/*
_curry(func)
功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供,则执行func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数
返回值:柯里化后的函数
// 柯里化可以将多元函数转化为一元函数
function getSum (a, b, c) {} //三元函数
function getSum (a, b) {} // 二元函数
function getSum (a, b) {} // 一元函数
*/
function getSum (a, b, c) {
return a + b + c;
}
const curried = _.curry(getSum);
console.log(curried(1,2,3)); // 6
console.log(curried(1)(2,3)); // 6
const match = _.curry(function (reg, str) {
return str.match(reg);
});
const haveSpace = match(/\s+/g);
console.log(haveSpace('hello world'));
// 模拟lodash中的柯里化 curry 方法
function curry (func) {
return function curriedFn (...args) {
// ES6的剩余参数 ...args
// 判断 形参个数 和实参个数是否相同 形参是传入的func的入参个数;实参是arguments的长度
if (args.length < func.length) {
return function (...moreArgs) {
return curriedFn(...args.concat(moreArgs));
// return curriedFn(...args.concat(Array.from(arguments)));
// ...args上级函数的入参,arguments是当前函数入参,两个入参需要拼接在一起,arguments是伪数组,所以需要转化为数组,concat进行拼接
}
}
return func(...args);
}
}
- lodash中的组合函数 flow() 和flowRight() (flow()从左到右运行,flowRight()从右到左运行)
- lodash中提供的方法如split、map都是数据优先函数置后的原则;而lodash的fp模块中的方法如split、map是函数优先数据置后的原则。
// lodash中的 函数组合方法 flowRight()
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
const f = lodash.flowRight(toUpper, first, reverse);
console.log(f(['one', 'two'])); // 输出: TWO
6. 柯里化
当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数之后不变),然后返回一个新的函数接收剩余的参数,返回结果
柯里化生成新的函数,新的函数已经记住了某些固定的参数
柯里化是对函数参数进行缓存,使用闭包了;柯里化让函数变得更灵活,让函数的粒度更小;可以把多元函数转换为一元函数
// 普通的纯函数
function checkAge (min, age) {
return age > min;
}
// 函数的柯里化
function checkAge (min) {
return function (age) {
return age > min;
}
}
// ES6 写法
const checkAge = min => ( age => (age > min));
函数组合
如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数,这就是函数组合。函数组合默认从右到左 执行
函数组合要满足结合律
( f(x) g(x) h(x) 既可以将f和g组合,也可以将g和h组合,结果都一样,这种叫组合率)
function reverse (array) {
return array.reverse();
}
function first (array) {
return array[0];
}
// 函数组合
function compose (f, g) {
return function (value) {
// 这里如果是单个参数用value,如果是多个参数用剩余参数语法 ...args
return f(g(value));
}
}
const last = compose (first,reverse);
console.log(last([1,2,3,4])); // 输出: 4
// 组合函数 原理模拟
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
// function compose (...args) {
// return function (value) {
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc);
// }, value)
// }
// }
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc),value);
// fn 表示 从传入的函数数组 args 中取出的每一个函数
Point Free
是一种编程风格,具体实现:函数的组合,更为抽象
可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// Point Free模式
// 导入 lodash的fp模块
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower);
console.log(f('hello world')); // 输出: hello_world
// world wild web 要输出 W. W. W
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '));
const firstLetterToUpper = fp.flowRight(fp.map(fp.join('. '), fp.flowRight(fp.first, fp.toUpper)), fp.split(' '));
functor (函子可以帮助我们控制副作用,进行异常处理或者异步操作)
通俗来说,函子的概念是:把函子想象为一个盒子,这个盒子里面包裹着一个值,想要对这个值进行处理的话,需要调用这个盒子提供的map方法,map方法接收一个函数类型的参数,我们传递的这个函数就是处理值的函数
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
函数式编程的运算不直接操作值,而是由函子完成
函子就是一个实现了map契约的对象
可以把函子想象成一个盒子,这个盒子封装了一个值
想要处理盒子中的值,需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
最终map方法返回一个包含新值的盒子(函子)
- MayBe函子 (处理空值的异常,入参是值)
- 可以去处理入参为空值的问题,但是如果多次调用map方法时,哪次调用的是null是不明确的
- Either函子 (入参是值)
- 使用either函子时如何定义两个类
- either 两者中的任何一个,类似于if…else…的处理
- either函子可以用来做异常处理
- IO函子 (inputoutput 输入输出),入参是函数, 使用IO函子延迟执行一个函数
- IO函子中的 _value是一个函数 ,这里把函数作为值来处理
- IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
- 把不不纯的操作交给调用者来处理
- Task函子 (进行异步处理,帮助处理异步的任务)
- monad函子 (帮忙解决函子嵌套的问题)一个函子中如果具有join方法和of方法两个方法时并遵守一些规律就是monad
// functor 函子
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value;
}
map (fn) {
return new Container.of(fn(this._value))
}
}
let r = new Container.of(5)
.map(x => x + 1)
.map(x => x * x)
// MayBe 函子
class MayBe {
// 为让外部创建 MayBe 函子方便些,设置一个静态方法 of
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('hello world')
.map(x => x.toUpperCase())
// 输出结果:MayBe { _value: 'HELLO WORLD'}
let r = MayBe.of(null)
.map(x => x.toUpperCase())
// 输出结果: MayBe { _value: null}
// Either 函子
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this
}
}
class Right {
// 静态方法 of
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value;
}
map (fn) {
return Right.of(fn(this._value))
}
}
let r1 = Right.of(12).map(x => x + 2) // 输出: Right: { _value: 14 }
let r2 = Left.of(12).map(x => x + 2) // 输出: Left: { _value: 12 }
function parseJSON (str) {
try {
return Right.of(JSON.parse(str));
} catch (e) {
return Left.of( { error: e.message } )
}
}
// IO函子
class IO {
static of (x) {
return new IO(function () {
return x;
})
}
constructor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp, flowRight(fn, this._value))
}
}