一. 函数式编程
1、为什么要使用用函数式编程(Function Programming, FP)
- 流程框架逐步拥抱,React、Vue3都使用了函数式编程
- 方便tree shaking 过滤无用的代码
- 不用再关注this
- 方便测试以及并行处理
- 多种库帮助开发:lodash、underscore、ramda
2、函数式编程原则
- 不保存中间的计算结果,变量是不可以变的
- 更细粒度的纯函数,有很多函数式编程的库帮助开发, 不用自己开发细粒度的纯函数
- 使用函数组合,将多个细粒度的函数组合起来生成新的函数,来实现复杂的函数功能。
3、编程思想
- 面向过程:即按照步骤一步步实现
- 面向对象:将事物抽象成程序中的类或对象、通过封装、继承和多态来描述事物之间的联系
- 函数式编程:对运算过程的封装,描述的是事物的映射关系。
4、First-class Function:主要体现在以下几个方面
- 函数可以赋值给变量
- 函数可以作为参数,这样我们只要关注结果,处理过程由调用者定义,更加灵活
- 函数可以作为返回值
5、高阶函数:接收函数作为参数或返回函数的函数
6、闭包:可以返回外部函数变量的函数,作用:
- 延长外部函数变量作用域,在外部函数执行结束之后仍然可以访问外部函数的变量。但可能导致内存溢出,因为闭包内引用的外部函数变量得不到释放。
- 变量私有化,保证变量不被污染。只有通过闭包才能访问外部函数的变量,避免了变量被随意修改。
7、纯函数:对于相同的输出,始终会输出相同的结果。即连续多次调用,输出的结果相同。
优点:
- 可缓存:复杂计算时缓存计算结果,如lodash.memoize 会返回一个缓存计算结果的函数。
- 可测试:函数形式方便测试,因为函数具有输入输出
- 方便并行处理:多线程环境下操作共享内存数据可能出现意外,ES6 也增加了web worker 也可以进行多线程任务,而纯函数只依赖传入的参数,不需要访问共享的内存数据,所以并行环境下可以任意运行存函数
8、副作用:能够让纯函数变的不纯,来源:
- 全局变量
- 文件
- 异步数据
9、柯里化(Haskell Brooks Curry):当一个函数有多个参数时,可以将函数优化成只接受部分参数,并返回一个接受其他剩余参数并返回想要结果的函数。
function checkAge(age, min) {
return age >= min;
}
const makeCheckAge = min => age => age >= min;
上面的示例代码展示的checkAge的柯里化过程,makeCheckAge接收一个参数,并返回一个接收另一个参数的函数并返回结果的函数。
为什么要柯里化:
- 缓存函数参数,根据需求生成固定参数的新函数
- 将多元函数转化成粒度更小的一元函数,方便后续函数组合
柯里化实现原理:
- 传递了部分参数,需要返回一个接收剩余参数的函数
- 传递了所有参数,直接返回func 的执行结果
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1, 2, 3));
console.log(curriedSum(1, 2)(3));
console.log(curriedSum(1)(2, 3));
console.log(curriedSum(1)(2)(3));
function curry(func) {
return function curriedFn(...args) {
// 两种情况,传递了部分参数和传递了所有参数
if (args.length < func.length) { // 传递了部分参数,需要返回一个接收剩余参数的函数
return function (...argus) {
return curriedFn(...args, ...argus);
}
}
// 传递了所有参数,直接返回func 的执行结果
return func(...args);
}
}
10、函数组合
将多个函数组合成一个函数,并将函数的返回结果一次传递给下一个函数
函数组合实现原理:
- 返回一个函数
- 从右向左依次执行参数函数,并将结果传递给下一参数
const arr = ['one', 'two', 'three'];
const lastUpper = _.flowRight(_.toUpper, _.first, _.reverse);
const compose = (...funcs) => val => funcs.reverse().reduce((preResult, next) => next(preResult), val);
console.log(lastUpper(arr)); // THREE
函数结合律: 可以先组合前面联系的部分也可以先组合后面连续的部分函数。
const arr = ['one', 'two', 'three'];
const lastUpper = compose(compose(_.toUpper, _.first), _.reverse);
// 等价
const lastUpper2 = compose(_.toUpper, compose(_.first, _.reverse));
函数组合要求传入的函数必须是纯函数
// 打印每个阶段处理之后的结果
const trace = curry((tag, v) => {
console.log(tag, v);
return v;
});
const str = "NEVER GIVE UP"; // 将字符串按空格切分成数组,并将数组的每一项转换成小写
// 将字符串按空格切分成数组,使用lodash的split方法,但 split 接收两个参数,需要柯里化成一元函数
const split = curry((sep, str) => _.split(str, sep)); // 需要借助柯里化生成一个按空格分隔字符串的函数,所以参数位置需要改变
// 将数组的每一部分转换成小写,需要通过lodash的map方法实现,map接收两个参数,array 和 func, 需要柯里化成一元函数
// 需要借助柯里化生成一个按指定方法处理数组,并返回处理结果集的函数,所以参数位置需要改变
const map = curry((func, arr) => _.map(arr, func));
// 将数组按指定的连接符连接成一个字符串,需要通过lodash的join方法,join方法接收两个参数 arr 和 sep, 需要柯里化成一元函数
const join = curry((sep, arr) => _.join(arr, sep)); // 需要借助柯里化生成一个按连接符连接的字符串的函数,所以参数位置需要改变
const lowerSepStr = compose(join('-'), trace('map'), map(_.toLower), trace('split'), split(' '));
console.log(lowerSepStr(str)); // never-give-up
11、Lodash中的fp模块
- 提供了对函数式编程友好的方法
- 提供的函数都是不可变的,柯里化之后的,函数优先,数据滞后的函数
const fp = require('lodash/fp');
const lowerSepStr = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '));
console.log(lowerSepStr(str)); // never-give-up
- lodash中的map方法和fp中的map方法的区别
lodash中map方法的参数函数接收3个参数,val、index/key、collection,fp中的map方法的参数函数只接收1个函数,val
12、函子 Functor:函子就是一个容器,里面包含一个私有属性保存值,并对外提供一个map方法,接收一个方法用于处理值,并返回一个新的函子。
class Container {
static of(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.of(fn(this._value));
}
}
IO 函子:接收一个函数作为值,延迟执行,map方法中要组合函数
class IO {
static of (val) {
return new IO(function () {
return val;
})
}
constructor(func) {
this._value = func;
}
join() {
return this._value();
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
flatMap(fn) {
return this.map(fn).join();
}
}
function readFile(path) {
return new IO(function () {
return fs.readFileSync(path, 'utf-8');
})
}
Folktale:函数编程辅助库,https://folktale.origamitower.com/docs,提供了一系列可用的函子。
task方法:Folktale中提供辅助异步编程的函子,用法类似于promise,参数为一个方法,该方法接收一个resolver参数,包含resolve和reject两个方法,分别在执行成功和失败时调用。task 方法返回一个task函子,包含一个run 方法,调用run方法时,开始执行task。
function readFile(path) {
return task(resolver => {
fs.readFile(path, 'utf-8', (err, data) => {
resolver.resolve(data);
})
console.log('read file');
})
}
Pointed函子:包含of方法的函子。
Monad函子:包含of方法和join 方法并遵循一定规律的函子。解决嵌套函子调用不方便的问题。在函数返回函子时使用
- map:组合函数时,函数返回值时调用
- flatMap:组合函数时,函数返回函子时调用
function readFile(path) {
return new IO(function () {
return fs.readFileSync(path, 'utf-8');
})
}
function print(x) {
return new IO(function () {
console.log(x)
return x;
})
}
const r = readFile('chunk.js')
.flatMap(print)
.join();
二. 异步编程
回调函数是所有异步操作的根本,但是回调函数方式不利于阅读,阅读时执行逻辑相对混乱,存在回调地狱问题。
捕获全局中未被捕获的异常,浏览器中通过在window中注册unhandledrejection 事件。
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event;
}, false)
宏任务:需要重新进入到任务队列中排队的任务,如setTime,setInterval等
微任务:不需要进入到任务队里中重新排队的任务,会在本轮调用末尾执行,例如:Promise,MutationObsever以及process.nextTick
generator:支持以同步的方式编写异步代码
generator 执行器函数:
function co(generator) {
const g = generator();
function handleResult(r) {
if (r.done) return;
r.value.then(res => {
handleResult(g.next(res))
}, err => {
g.throw(err);
})
}
handleResult(g.next());
}
Async和await是generator 函数的语法糖,优点是可以不用手动书写执行器函数。
async function fetch() {
try {
const r1 = await ajax('./api/test.json');
console.log(r1);
const r2 = await ajax('./api/test1.json');
console.log(r2);
} catch (e) {
console.log(e);
}
}