前言

背景引入

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
// 问题: 上接浏览器原理 —— 参数 parse
// 1. 数组在url中展示形式
// location.search => '?name[]=progressive$%coding&name[]=objective$%coding&name[]=functional$%coding'
// 2. 参数提取拼接成数组
// ['progressive$%coding', 'objective$%coding', 'functional$%coding']
// 3. 手写方法,转换成数组对象
// [{name: 'Progressive Coding'}, {name: 'Objective Coding'}, {name: 'Functional Coding'}]

const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];
const _objArr = [];

const nameParser = (array, objArr) => {
array.forEach(item => {
let names = item.split('$%');
let newName = [];

names.forEach(name => {
let nameItem = name[0].toUpperCase() + name.slice(1);

newName.push(nameItem);
})
objArr.push({
name: newName.join(' ');
});
})
return objArr
}

console.log(nameParser(_array, _objArr));

// 问题:
// 1. 过程中存在逻辑包裹 - 看完整段代码,才能明白在做啥
// 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
28
29
30
31
32
33
34
35
36
37
38
39
40
// step1. 需求分析 => 数组 > 数组对象 => [字符串 > 对象]
// nameParser => [objHelper :: string > object]

// step2. 模块功能明确 => objHelper = formatName + assembleObj

// step3. 功能拆分 => objHelper = [(split + capitalize + join)] + assembleObj

// step4. 代码实现
// 输入
const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];

// 原子操作
const assembleObj = (key, x) => {
let obj = {};

obj[key] = x;
return obj;
}
const capitalize = name => name[0].toUpperCase() + name.slice(1);

// 组装描述
const formatName = R.compose(join(' '), map(capitalize), split('$%'));
const objHelper = R.compose(assembleObj('name'), formatName);
const nameParser = map(objHelper);

// 使用
nameParser(_array);

// 问题:正确的遍历 - for forEach (map (filter sort))……
// 本质作用 => 通用遍历 | 遍历逻辑处理 | 生成数组 - 处理后 | 生成数组 - 处理后 - 过滤 | 生成数组 - 处理后 - 排序

let class = 'functional';
let isOvered = false;

// forEach
classArr.forEach(item => {
isOvered = item === class;
})

classArr.map(item => item.name === 'functional')

函数式编程特点

函数: 一等公民,辑功能实现的落脚点
声明式编程 => 声明需求 => 语义化
惰性执行 - 无缝连接,性能节约

本质

  • 加法结合律 | 因式分解 | 完全平方公式 => a + b + c = (a + b) + c 原子组合的变化
  • 水源 => 组合(水管 + 走线)=> 浴缸

理论思想

  1. 函数是一等公民:函数可以作为参数传递给其他函数或作为返回值返回。
  2. 纯函数:函数的输出仅由输入决定,不会对外部状态产生影响。
  3. 没有副作用:函数不会修改程序状态或全局变量,只会返回新的值。
  4. 高阶函数:函数可以接受其他函数作为参数,也可以返回一个函数。
  5. 数据不可变性:数据一旦创建就不能被修改。因此,函数式编程通常使用不可变数据结构,例如列表、字典、集合等。函数式编程还鼓励使用递归而不是循环,因为递归更符合函数式编程的思想。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 惰性函数
let program = (name) => {
if (name === "progressive") {
return (program = () => {
console.log("progressive");
});
} else if (name === "objective") {
return (program = () => {
console.log("objective");
});
} else {
return (program = () => {
console.log("functional");
});
}
};

program("progressive")();
console.log("lazy");
program(); // 函数提升
// progressive lazy progressive

无状态与无副作用 - rxjs

  • 无状态 - 幂等;数据不可变 - 不可操作改变源数据
  • 无副作用 - 函数内部不应该直接对整个系统中任何参数变量做改动

优缺点

  1. 优点:变量不可变,引用透明,天生适合并发。表达方式更加符合人类日常生活中的语法,代码可读性更强。实现同样的功能函数式编程所需要的代码比面向对象编程要少很多,代码更加简洁明晰。函数式编程广泛运用于科学研究中,因为在科研中对于代码的工程化要求比较低,写起来更加简单,所以使用函数式编程开发的速度比用面向对象要高很多,如果是对开发速度要求较高但是对运行资源要求较低同时对速度要求较低的场景下使用函数式会更加高效。
  2. 缺点:由于所有的数据都是不可变的,所以所有的变量在程序运行期间都是一直存在的,非常占用运行资源。同时由于函数式的先天性设计导致性能一直不够。虽然现代的函数式编程语言使用了很多技巧比如惰性计算等来优化运行速度,但是始终无法与面向对象的程序相比,当然面向对象程序的速度也不够快。函数式编程虽然已经诞生了很多年,但是至今为止在工程上想要大规模使用函数式编程仍然有很多待解决的问题,尤其是对于规模比较大的工程而言。如果对函数式编程的理解不够深刻就会导致跟面相对象一样晦涩难懂的局面。

vs POP

  1. 状态和副作用的处理方式不同:过程式编程中,通常会有多个全局变量,函数会修改这些全局变量的值,从而实现状态的更新和副作用的产生。而函数式编程中,函数通常不会修改传入的参数和外部状态,它们只是根据输入计算输出,避免了副作用。
  2. 数据结构的处理方式不同:过程式编程中,通常会使用诸如数组和链表等数据结构,这些数据结构可以在程序的执行过程中被修改和操作。而函数式编程中,通常会使用不可变的数据结构,例如列表和元组,这些数据结构在创建之后不可变,任何修改操作都会返回一个新的数据结构。
  3. 控制流的处理方式不同:过程式编程中,通常会使用循环、条件分支等控制流结构,这些结构会改变程序的执行顺序和流程。而函数式编程中,通常会使用递归和高阶函数等技术来控制程序的执行流程。
  4. 函数的作用不同:过程式编程中,函数通常被视为一系列指令的集合,用来完成某个具体的任务。而函数式编程中,函数通常被视为一种映射关系,用来将输入映射为输出,函数的作用是描述输入和输出之间的关系。

vs OOP

  1. 数据和行为的处理方式:在 OOP 中,数据和行为通常是紧密耦合的,一个对象包含一些属性和方法来操作这些属性。在函数式编程中,数据和行为通常是分离的,函数只处理输入数据,而不改变它们的状态。
  2. 程序的设计和组织方式:在 OOP 中,程序通常是由一些对象组成,每个对象负责处理一些任务。在函数式编程中,程序通常是由一些函数组成,每个函数负责处理一个特定的问题。
  3. 程序的可读性和可维护性:函数式编程通常使用不可变的数据结构,这可以使程序更加可读和可维护。同时,函数式编程也通常使用纯函数,这可以使程序更加可靠和可测试。
  4. 各有优缺点:在处理递归问题时,函数式编程可以比面向对象编程更加简洁和自然;而在处理大规模、复杂的系统时,面向对象编程可以提供更好的组织和抽象能力。

实际开发

纯函数改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const _class = {
name: "objective",
};
// 函数内部引入外部变量后 —— 违反了无状态
const score = (str) => _class.name + ":" + str;

// 修改了传入参数 —— 无副作用
const changeClass = (obj, name) => (obj.name = name);

changeClass(_class, "functional");
score("good!");

// #######################
const _class = {
name: "objective",
};

const score = (obj, str) => obj.name + ":" + str; // 不依赖外部变量
const changeClass = (obj, name) => ({ ...obj, name }); // 未修改外部变量

changeClass(_class, "functional");
score(_class, "good!");

流水线组装 - 加工 & 组装

a. 加工 - 柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// f(x, y, z) -> f(x)(y)(z)
const sum = (x, y) => {
return x + y;
};
sum(1, 2);

const add = (x) => {
return (y) => {
return x + y;
};
};
add(1)(2);

// 实现 体系 = 加工 + 组装,单个加工输入输出应当单值化 -> 需求
const fetch = ajax(method, url, params);

const request = ajax(method);
const fetch = request(url);
ajax(method)(url)(params);

问题: 问题:手写构造可拆分传参的累加函数 add(1)(2)(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
// 1. 构造科里化结构
// 2. 输入 处理外部arguments => 类数组形态处理
// 3. 传入参数无限拓展 => 递归 内层逻辑 => 返回函数
// 4. 主功能实现 => 累加
// 5. 输出

const add = function () {
// 输入
let args = Array.prototype.slice.call(arguments);

// 内层处理
let inner = function () {
args.push(...arguments); // 内外层参数合并
return inner;
};

inner.toString = function () {
return args.reduce((prev, cur) => {
return prev + cur;
});
};

return inner;
};

"" + add(1)(2)(3)(4); // '10'
parseInt(add(1)(2)(3)(4), 10); // 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function add() {
let args = [...arguments];

let inner = function () {
args.push(...arguments);

if (![...arguments].length) {
return args.reduce((prev, cur) => {
return prev + cur;
});
}

return inner;
};

return inner;
}

console.log(add(1)(10, 100)(1000)());

b. 流水线 - 组装函数

1
2
3
4
5
6
const compose = (f, g) => (x) => f(g(x));

const sum1 = (x) => x + 1;
const sum2 = (x) => x + 2;
const sum12 = compose(sum1, sum2);
sum12(1);

实际实现使用

1
2
3
4
5
6
7
8
9
// 命令式
trim(reverse(toUpperCase(map(arr))));

// 面向对象
arr.map().toUpperCase().reverse().trim();

// 函数式
const result = compose(trim, reverse, toUpperCase, map);
pipe(map, toUpperCase, reverse, trim); // history | grep rm

BOX 与 函子

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
// 一封信
class Mail {
constructor(content) {
this.content = content;
}
map(fn) {
return new Mail(fn(this.content));
}
}

// 1. 拆开信
let mail1 = new Mail("love");
// 2. 读了信
let mail2 = mail1.map(function (mail) {
return read(mail);
});
// 3. 烧了信
let mail3 = mail1.map(function (mail) {
return burn(mail);
});
// 4. 老师查寝时候
mail3.map(function (mail) {
return check(mail);
});

// 链式
new mail("love").map(read).map(burn).map(check);