函数式编程范式
注:本文部分代码涉及到一些 JavaScript 新特性,需要提前了解一下: JavaScript 新特性
讲解视频: https://www.bilibili.com/video/BV1vV411a7kH/
1 函数式编程范式
1.1 定义
函数式编程(Functional Programming: FP)是一种编程范式(指计算机中编程中的典范模式或方法,就是一种思维方式),属于结构化编程,用于描述数据(函数)之间的映射关系。
特别需要注意的是,函数式编程中的函数不是指程序中的函数(方法),而是数学中的函数(映射关系),如:
y = f(x)
,指x
和y
之间的关系。常见的编程范式有:过程化(命令式)编程、面向对象编程、声明式编程等。
过程化编程:最原始的传统编程,将问题抽象为一系列步骤,然后通过编程方式将这些步骤转换为程序指令集,这些指令集按照一定顺序排列。人们把支持过程化编程范式的编程语言称为过程化编程语言,常见的有机器语言、汇编语言、BASIC、C、FORTRAN 等。过程化语言特别适合解决线性(或者说按部就班)的算法问题。
面向对象编程:将待解决问题抽象为面向对象的程序中的对象,利用封装使每个对象都拥有个体的身份。程序就是成堆的对象,彼此通过信息的传递,请求其它对象进行工作。面向对象包括三个基本概念:封装性、继承性、多态性。常见的面向对象语言有 Java、C、C++、JavaScript。
声明式编程:以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子,我们只需要定义好该如何处理数据,不需要指定具体实现,就可以查询到我们需要的数据。
现代编程语言的发展趋势是支持多种范式,如 C#、Java 8+、Kotlin、ES6+。
编程范式和设计模式的区别 :
编程范式:是指从事软件工程的一类典型的编程风格(此概念好比“战略”),体现编写程序的人如何看待程序设计的“哲学观”; 程序设计模式:设计模式是软件设计中常见问题的典型解决方案(此概念好比“战术”),是解决一系列实际问题的“方法学”。 1.2 特点
代码简洁:函数式编程使用了大量的函数,减少了代码的重复;
接近自然语言,易于理解:
1
2
3 let result = (1 + 2)* 3 - 4; // 普通表达式
let result = subtract(multiply(add(1, 2), 3), 4); // 函数式编程函数是“第一等公民”:函数与其他数据类型一样,处于平等地位,可以赋值给其它变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值;
闭包和高阶函数:函数式编程会使用较多的闭包和高阶函数;
没有“副作用”,方便与代码管理和单元测试:
副作用
指函数内部与外部互动(最典型的情况,就是修改全局变量量的 值),产⽣运算以外的其他结果。函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他⾏为,尤其是不得修改外部变量的值;引用透明:函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
1.3 基本概念
把现实世界的事物和事物之间的联系(映射关系)抽象到程序世界(对运算过程进行抽象)
1
2 // 比如买单价为0.5元的白菜,买了两斤,需要支付多少块钱(白菜与货币的联系)
let money = multiply(0.5, 2); // 即两斤白菜 -> 1元(money)② 程序的本质:
根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数。
③ 函数
y = f(x)
:x → f(映射) → y
图 1 从 x 到 y 的函数关系
图 2 从 x 到 y 不是函数关系
④ 纯函数:相同的输入始终要得到相同的输出
⑤ 函数式编程是用来描述数据(函数)之间的映射
1.4 学习指南
函数式编程范式只是一种对程序编程思维的一种概论,而具体的实现则通过柯里化(第 5 章)、函数组合(第 6 章)、函子等来实现。
在学习如何实现前,需要先了解三个小知识点:头等函数(第 2 章),闭包(第 3 章),纯函数(第 4 章)。
2 头等函数
2.1 函数是一等公民
- 函数可以存储在变量中
- 函数可以作为参数(2.2.1)
- 函数可以作为返回值(2.2.2)
JavaScript 对待不同的数据结构具有同等级别的支持,函数可以享受以上几种待遇,所以在 JavaScript 中,函数是一等公民。
函数可以存储在变量中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 // 把函数赋值给变量
const fn = () => {
console.log("2.1 函数可以存储在变量中");
};
fn();
// 属性赋值示例
const objController = {
getKeys(obj) {
// ES6属性简写,等同于getKeys: getKeys(obj)
return Object.keys(obj);
},
};
// 优化:上面代码中getKeys方法和内部调用Object.keys方法的参数和返回值一样,所以可以改写成下面
const objController = {
getKeys: Object.keys,
};
console.log(objController);
const my = { name: "Patrick Jun", constellation: "Virgo" };
console.log(objController.getKeys(my));2.2 高阶函数
如果一个函数以下面任一方式使用,那么这个函数就可以称为高阶函数。
- 参数是一个函数
- 返回值是一个函数
Patrick Jun:可以操作函数的函数就是高阶函数。这就跟高数里的求导(二阶及以上的求导称之为高阶求导)一样,可以对已导函数的求导就是高阶求导。
2.2.1 函数作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 // 遍历(模拟数组的forEach方法)
function forEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i], i); // 将每一项传入回调fn处理
}
}
// 筛选,返回符合条件的元素组成的新数组
function filter(arr, fn) {
const results = [];
for (const item of arr) {
if (fn(item)) {
results.push(item);
}
}
return results;
}
const colors = ["#FF0000", "#00FF00", "blue"];
forEach(colors, (item, index) => {
console.log(index + 1, item);
});
console.log(filter(colors, (item) => item.length === 7));2.2.2 函数作为返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function makeFn() {
const msg = "hello function";
return function () {
console.log(msg);
};
}
const fn = makeFn(); // makeFn()执行后返回一个匿名函数,赋值给fn
fn();
// makeFn()()
// 模拟lodash中的once函数 对一个函数只执行一次的函数(例如支付,不管用户点击多少次按钮,都只执行一次)
function once(func) {
let done = false; // 定义一个状态done,判断是否已执行支付
return function () {
if (!done) {
done = true; // 更改闭包作用域中的done为已支付
func.apply(this, arguments);
}
};
}
const pay = once((money) => {
// 传入一个函数,通过输出模拟支付过程和结果
console.log(`支付${money}元`);
});
pay(20); // 支付20元
pay(30);
pay(40);2.2.3 使用高阶函数意义
抽象可以帮我们屏蔽细节,只需要关注我们的目标
比如前面的例子:不用在乎如何遍历,只需要关注我们怎么出处理数据。不用在乎用户会不会多次点击,只需要关注如何处理支付后的流程。
高阶函数用来抽象通用的问题
比如前面抽象遍历问题
2.2.4 常用高阶函数模拟
- map 通过指定函数处理数组的每个元素,并返回处理后的数组。
1
2
3
4
5
6
7
8
9
10
11 function map(arr, fn) {
const res = [];
for (const val of arr) {
res.push(fn(val)); // 将回调fn()处理好的元素存入新数组
}
return res;
}
let arr = [1, 2, 3, 4, 5];
arr = map(arr, (item) => item * item);
console.log(arr); //[ 1, 4, 9, 16, 25 ]- every 用于检测数组所有元素是否都符合指定条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 function every(arr, fn) {
let res = true; // 定义一个flag
for (const val of arr) {
res = fn(val); // fn判断
if (!res) {
// 只要有一个元素不满足,就结束循环
break;
}
}
return res;
}
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [4, 5, 6, 7];
const res1 = every(arr1, (item) => item > 3);
console.log(res1); // false
const res2 = every(arr2, (item) => item > 3);
console.log(res2); // true- some 判断数组中是否至少有一个元素满足条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 function some(arr, fn) {
let res = false; // 定义一个flag
for (const val of arr) {
res = fn(val); // fn判断
if (res) {
// 只要有一个元素满足,就结束循环
break;
}
}
return res;
}
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [1, 3, 5, 7];
const res1 = some(arr1, (item) => item % 2 === 0);
console.log(res1); // true
const res2 = some(arr2, (item) => item % 2 === 0);
console.log(res2); // false- find 返回数组中满足提供的测试函数的第一个元素的值,如果未找到,则返回 undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 function find(arr, fn) {
for (const item of arr) {
if (fn(item)) {
// 找到满足条件的第一个元素
return item;
}
}
return undefined; // 未找到返回undefined
}
const arr1 = [1, 2, 3, 4, 5];
const res1 = find(arr1, (item) => item % 2 === 0);
console.log(res1); // 2
const res2 = find(arr1, (item) => item === 8);
console.log(res2); // undefined- findIndex 找到满足条件的第一个元素,返回其位置,如果未找到,则返回-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 function findIndex(arr, fn) {
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i])) {
// 找到满足条件的第一个元素位置
return i;
}
}
return -1; // 未找到返回-1
}
const arr1 = [1, 2, 3, 4, 5];
const res1 = findIndex(arr1, (item) => item % 2 === 0);
console.log(res1); // 1
const res2 = findIndex(arr1, (item) => item === 8);
console.log(res2); // -13 闭包
3.1 定义
- 可以在另一个作用域中调用一个函数内部的函数并访问到该函数的作用域中的成员;
- 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕后会从执行栈上删除, 但是堆上作用域成员因为被外部引用而不能被释放 ,因此内部函数依然可以访问到作用域的成员;
函数嵌套函数(高阶函数)
函数内部可以引用函数外部的参数和变量
参数和变量不会被垃圾回收机制回收
3.2 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 function makePower(power) {
return function (number) {
return number ** power; // number为底数,power为指数
};
}
// 平方:number**2
const power2 = makePower(2);
// 立方:number**3
const power3 = makePower(3);
console.log(power2(5));
console.log(power2(2));
console.log(power3(4));
function makeSalary(base) {
return function (performance) {
return base + performance;
};
}
// 底层打工人
const level1 = makeSalary(1000);
// 高级打工人
const level2 = makeSalary(10000);
console.log(level1(100)); // 1100
console.log(level1(120)); // 1120
console.log(level2(30000)); // 40000打开 Chrome 开发者工具 > Sources :
- Call Stack(函数调用栈)
- Scope(作用域) : Global(var 全局) 、 Local(局部) 、 Closure(闭包) 、 Script(let 作用域)
仅看一看演示一下,具体细节之后专门分享 ^_^
1
2
3
4
5
6
7
8
9
10
11
12
13 // 查看函数栈和闭包作用域成员的访问
function makeSalary() {
let base = 1000;
return function (performance) {
// debugger;
base += 1;
return base + performance;
};
}
const sallary = makeSalary();
console.log(sallary(100));
console.log(sallary(200));4 纯函数
4.1 概念
- slice 和 splice 分别:纯函数和不纯函数
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作返回该数组,会改变原数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 // 纯函数 slice(start, end)
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ]
console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ]
console.log(numbers.slice(0, 3)); // [ 1, 2, 3 ]
// 不纯函数 splice(index, howmany, ...items)
console.log(numbers.splice(0, 3)); // [ 1, 2, 3 ]
console.log(numbers.splice(0, 3)); // [ 4, 5 ]
console.log(numbers.splice(0, 3)); // []
// 最简单的纯函数示例
function getSum(a, b) {
return a + b;
}
console.log(getSum(1, 2)); // 3
console.log(getSum(1, 2)); // 3
console.log(getSum(1, 2)); // 34.2 lodash
官网: lodash
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 const _ = require("lodash");
const arr = ["Tom", "Jon", "Kate"];
console.log(_.first(arr));
console.log(_.last(arr));
console.log(_.toUpper(_.last(arr)));
console.log(_.reverse(arr));
console.log(_.first(arr));
_.each(arr, (item, index) => {
console.log(item, index);
});
const value = [];
_.isEmpty(value); // 判断一个value 是否是empty(null,[],{}....)4.3 纯函数的好处
- 可缓存:因为纯函数相同的输入永远会等到相同的输出,所以可以把纯函数结果缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 // 记忆函数
const _ = require("lodash");
function getArea(r) {
console.log(`执行getArea计算,r = ${r}`);
return Math.PI * r * r;
}
// 这里使用lodash中的记忆函数
const getAreaWithMemory = _.memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4)); // 不会再次计算
console.log(getAreaWithMemory(5));
// js模拟 memoize 方法的实现
function memoize(f) {
const cache = {};
return function () {
const key = JSON.stringify(arguments);
cache[key] = cache[key] || f.apply(f, arguments);
return cache[key];
};
}
const getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(5));- 可测试:纯函数让测试更加方便,对单元化测试很友好
- 并行处理:在多线程环境下并行操作共享的内存数据很可能会出现意外情况,纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
4.4 副作用
纯函数:指 相同的输入永远会得到相同的输出 ,而且没有可观察的 副作用 ,而副作用让一个函数变的不纯,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
1
2
3
4
5
6
7
8
9
10
11 // 不纯的函数
let min = 18;
function checkAge(age) {
return age >= min; // 依赖外部的min状态
}
// 纯函数
function checkAge2(age) {
let min = 18; // 硬编码,可通过闭包或者柯里化解决
return age >= min;
}副作用的来源:
获取用户的输入:
所有的外部交互都有可能带来副作用,副作用也会使方法通用性下降、不适合扩展,同时副作用会给程序中带来安全隐患给程序员带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 // 有副作用
let result = 0;
function sum() {
const a = $(".input-1").val();
const b = $(".input-2").val();
result = a + b;
}
// <button onclick="sum()">求和</button>
// 避免副作用
function sum(a, b) {
return a + b;
}
$("button").bind("click", () => {
const a = $(".input-1").val();
const b = $(".input-2").val();
result = sum(a, b);
});
// <button>求和</button>5 柯里化
- 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接受剩余的参数,直达参数接收完毕才返回结果
5.1 柯里化示例(问题回顾)
1
2
3
4
5
6
7 // 普通纯函数的方式解决
function checkAge(age, min) {
return age >= min;
}
console.log(checkAge(20, 18)); // true
console.log(checkAge(17, 18)); // false
console.log(checkAge(24, 22)); // true上面代码可以发现当基准值时
18
时,18
是重复的 使用之前所学的闭包处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14 // 闭包的方式解决(简单的柯里化)
function checkAge(min) {
return function (age) {
return age >= min;
};
}
const checkAge18 = checkAge(18);
const checkAge22 = checkAge(22);
console.log(checkAge18(17)); // false
console.log(checkAge18(20)); // true
console.log(checkAge22(20)); // false
console.log(checkAge22(30)); // true使用 ES6 改造上面
checkAge
函数:
1 let checkAge = (min) => (age) => age >= min;5.2 lodash.curry(fn)
- _.curry(fn)
- 文档: https://www.lodashjs.com/docs/lodash.curry
- 功能:创建一个函数,该函数接受 fn 的参数。如果 fn 所需的参数都被提供则执行 fn 并返回结果,否则 继续返回该函数并等待接收剩余的参数 。
需要注意:传参先后顺序不能变
1
2
3
4
5
6
7
8
9
10
11 const _ = require("lodash");
function getSum(a, b, c) {
return a + b + c;
}
const curried = _.curry(getSum);
console.log(curried(2, 3, 4)); // 9
console.log(curried(2)(3)(4)); // 9
console.log(curried(2)(3, 4)); // 9
console.log(curried(2, 3)(4)); // 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 const _ = require("lodash");
const match = _.curry((reg, str) => {
return str.match(reg);
});
// 匹配所有数字
const hasSpace = match(/\s+/g);
// 匹配所有空白字符
const hasNumber = match(/\d+/g);
console.log(hasSpace("helloword")); // null
console.log(hasNumber("123213 123")); // ["123213", "123"]
console.log(hasNumber("helloword")); // null
// 再扩展:筛选数组中指定条件的元素
const filter = _.curry((func, array) => {
return array.filter(func);
});
console.log(filter(hasSpace, ["Patrick Jun", "Patrick_Jun"])); // ["Patrick Jun"]
// 分步使用 = filter(hasSpace)(['Patrick Jun', 'Patrick_Jun'])
const findSpace = filter(hasSpace);
console.log(findSpace(["Patrick Jun", "Patrick_Jun"])); // ["Patrick Jun"]模拟 lodash 中的 curry 方法
小知识点:
fn = (a, b, c, d, e) => {};
,那么fn.length = 5
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 function curry(func) {
return function curriedFn(...args) {
// 判断形参和实参的个数
if (args.length < func.length) {
return function () {
// 将...args与...arguments拼接传递给curriedFn
return curriedFn(...args, ...arguments);
};
}
return func(...args);
};
}
function getSum(a, b, c) {
return a + b + c;
}
const curried = curry(getSum);
console.log(curried(2, 3)(4)); // 9
console.log(curried(2)(3, 4)); // 9图解步骤:
5.3 总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的“缓存”(闭包)
- 让函数变的更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
6 函数组合
6.1 概念
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
- 函数组合后只接受一个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // 函数组合演示
function reverse(array) {
return array.reverse();
}
function first(array) {
return array[0];
}
function compose(f, g) {
return function (value) {
return f(g(value));
};
}
const last = compose(first, reverse);
console.log(last([1, 2, 3, 4])); // 46.2 lodash 组合函数
lodash 中组合函数
flow()
或者flowRight()
,他们都可以组合多个函数
flow
和flowRight
会创建一个函数,返回结果是调用提供函数的结果。提供函数会连续调用,每个提供函数传入的参数都是前一个函数返回的结果。flow()
是从左到右运行flowRight()
是从右到左运行,使用的更多一些
1
2
3
4
5
6
7
8
9 const _ = require("lodash");
const reverse = (arr) => arr.reverse();
const first = (arr) => arr[0];
const toUpper = (s) => s.toUpperCase();
const f = _.flowRight(toUpper, first, reverse);
console.log(f(["one", "two", "three"])); // ???模拟
lodash
中的flowRight()
方法:- 数组中的 reduce() :对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。
1
2
3
4
5
6
7
8
9
10 function compose(...args) {
return function (val) {
return args.reverse().reduce((acc, fn) => {
return fn(acc);
}, val);
};
}
// ES6
// const compose = (...args) => (val) => args.reverse().reduce((acc, fn) => fn(acc), val);图解步骤:
6.3 结合律
例如
compose(f,g,h)
,我们既可以先把f
和g
组合在一起,还可以先把g
和h
组合:
1
2
3
4
5
6
7
8
9
10
11
12 console.log(compose(compose(f, g), h) == compose(f, compose(g, h))); //true
console.log(compose(f, g, h) == compose(f, compose(g, h))); //true
const _ = require("lodash");
// 下面三种写法结果运行一样
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse); // 前两个组合
const f1 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse)); // 后两个组合
const f2 = _.flowRight(_.toUpper, _.first, _.reverse); // 不组合
console.log(f(["one", "two", "three"]) === f1(["one", "two", "three"])); // true
console.log(f(["one", "two", "three"]) === f2(["one", "two", "three"])); // true
console.log(f1(["one", "two", "three"]) === f2(["one", "two", "three"])); // true6.4 实战
题目:将
NEVER SAY DIE
转换为never-say-die
;思路:小写,分割,join
'NEVER SAY DIE'.toLowerCase().split(' ').join('-');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 const _ = require("lodash");
// 第一步:_.toLower()
// 第二步:_.split()
// 因为我们需要传入str变量,所以str放在最后面传入,以下同理
const split = _.curry((symbol, str) => _.split(str, symbol));
// 第三步:._join
const join = _.curry((symbol, array) => _.join(array, symbol));
// log用来检测数据管道中,哪部分值有错误
const log = (v) => {
console.log(v);
// 继续返回值给下一个fn
return v;
};
const f = _.flowRight(join("-"), log, split(" "), log, _.toLower);
console.log(f("NEVER SAY DIE")); // never-say-die
// // 考虑到数据管道很长的情况,如果多次log,打印的数据不够直观,于是改造log
// const _ = require('lodash');
// const trace = _.curry((tag, v) => {
// console.log(tag, v);
// return v;
// });
// const split = _.curry((symbol, str) => _.split(str, symbol));
// const join = _.curry((symbol, arr) => _.join(arr, symbol));
// const f = _.flowRight(join('-'), trace('after split:'), split(' '), trace('after toLower:'), _.toLower);
// console.log(f('NEVER SAY DIE'));- 函数式编程是一种强调以函数使用为主的软件开发风格;
- 纯函数指没有副作用的函数,相同的输入有相同的输出;
- 在函数式编程里面,将多个不同函数组合是一个非常非常非常重要的思想;
- 函数式编程将函数视为积木,通过一些高阶函数来提高代码的模块化和可重用性。
理解:柯里化是”因式分解“,将参数分解开;函数组合是”结合律“,函数可以组合使用。
进阶内容:
lodash/fp
、函子
;(笔者还没学明白呢,敬请期待)参考文章:
概念定义特点: https://juejin.im/post/6858129115598635015