一、概述
函数式编程是一种方案简单、功能独立、对作用域外没有任何副作用的编程范式:INPUT -> PROCESS -> OUTPUT
函数式编程:
1)功能独立 —— 不依赖于程序的状态(比如可能发生变化的全局变量);
2)纯函数 —— 同一个输入永远能得到同一个输出;
3)有限的副作用 —— 可以严格地限制函数外部对状态的更改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 函数返回表示“一杯绿茶(green tea)”的字符串
const prepareTea = () => 'greenTea';
/*
有一个函数(代表茶的种类)和需要几杯茶,下面的函数返回一个数组,包含字符串(每个字符串表示一杯特别种类的茶)。
*/
const getTea = (numOfCups) => {
const teaCups = [];
for(let cups = 1; cups <= numOfCups; cups += 1) {
const teaCup = prepareTea();
teaCups.push(teaCup);
}
return teaCups;
};
const tea4TeamFCC = getTea(2); // [ 'greenTea', 'greenTea' ]
|
二、了解函数式编程术语
需求有变更,现在想要两种茶:绿茶(green tea)和红茶(black tea)。 事实证明,用户需求变更是很常见的。
基于以上信息,需要重构上例中的 getTea 函数来处理多种茶的请求。
可以修改 getTea 接受一个函数作为参数,使它能够修改茶的类型。 这让 getTea 更灵活,也使需求变更时为程序员提供更多控制权。
首先,介绍一些术语:
Callbacks 是被传递到另一个函数中调用的函数。
例如在 filter 中,回调函数告诉 JavaScript 以什么规则过滤数组。
函数就像其他正常值一样,可以赋值给变量、传递给另一个函数,或从其它函数返回,这种函数叫做头等 first class 函数
。
在 JavaScript 中,所有函数都是头等函数。
将函数作为参数
或返回值
的函数叫做高阶 ( higher order) 函数
。
当函数被传递给另一个函数或从另一个函数返回时,那些传入或返回的函数可以叫做 lambda
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 函数返回表示“一杯绿茶(green tea)”的字符串
const prepareGreenTea = () => 'greenTea';
// 函数返回表示“一杯红茶(black tea)”的字符串
const prepareBlackTea = () => 'blackTea';
/*
有一个函数(代表茶的种类)和需要几杯茶,下面的函数返回一个数组,包含字符串(每个字符串表示一杯特别种类的茶)。
*/
const getTea = (prepareTea, numOfCups) => {
const teaCups = [];
for(let cups = 1; cups <= numOfCups; cups += 1) {
const teaCup = prepareTea();
teaCups.push(teaCup);
}
return teaCups;
};
const tea4GreenTeamFCC = getTea(prepareGreenTea, 1); // [ 'greenTea' ]
const tea4BlackTeamFCC = getTea(prepareBlackTea, 2); // [ 'blackTea', 'blackTea' ]
|
三、了解使用命令式编程的危害
使用 函数式编程 是一个好的习惯。
它使代码易于管理,避免潜在的 bug。
但在开始之前,先看看 命令式编程 方法,以说明可能会遇到什么问题。
在英语 (以及许多其他语言) 中,命令式 时常用来发出指令。 同样,命令式编程 是向计算机提供一套执行任务的声明。
命令式编程 常常改变程序状态,例如更新全局变量。 一个典型的例子是编写 for 循环,它为一个数组的索引提供了准确的迭代方向。
相反,函数式编程是声明式编程的一种形式
。 通过调用方法或函数来告诉计算机要做什么。
JavaScript 提供了许多处理常见任务的方法,所以无需写出让计算机应如何执行它们。
例如,可以用 map 函数替代上面提到的 for 循环来处理数组迭代。 这有助于避免语义错误,如调试章节介绍的 “Off By One Errors”。
考虑这样的场景:在浏览器中浏览网页,并想操作打开的标签。
下面试试用 面向对象 的思路来描述这种情景。
- 窗口对象由选项卡组成,通常会打开多个窗口。
- 窗口对象中每个打开网站的标题都保存在一个数组中。
- 在对浏览器进行了如打开新标签、合并窗口、关闭标签之类的操作后,需要输出所有打开的标签。
- 关掉的标签将从数组中删除,新打开的标签(为简单起见)则添加到数组的末尾。
代码编辑器中显示了此功能的实现,其中包含 tabOpen(),tabClose(),和 join() 函数。 tabs 数组是窗口对象的一部分用于储存打开页面的名称。
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
41
42
43
|
// tabs 是在窗口中打开的每个站点的 title 的数组
const Window = function(tabs) {
this.tabs = tabs; // 记录对象内部的数组
};
// 当将两个窗口合并为一个窗口时
Window.prototype.join = function(otherWindow) {
this.tabs = this.tabs.concat(otherWindow.tabs);
return this;
};
// 当在最后打开一个选项卡时
Window.prototype.tabOpen = function(tab) {
this.tabs.push('new tab'); // 现在打开一个新的选项卡
return this;
};
// 当关闭一个选项卡时
Window.prototype.tabClose = function(index) {
// 使用 splice()。这会产生副作用(对原始数组的更改),在实践中应该避免。
var tabsAfterIndex = this.tabs.splice(index);
const tabsBeforeIndex = this.tabs.splice(0, index); // 点击之前获取 tabs
const tabsAfterIndex = this.tabs.splice(1); // 点击之后获取 tabs
this.tabs = tabsBeforeIndex.concat(tabsAfterIndex); // 将它们合并起来
return this;
};
// 创建三个浏览器窗口
const workWindow = new Window(['GMail', 'Inbox', 'Work mail', 'Docs', 'freeCodeCamp']); // 邮箱、Google Drive 和其他工作地点
const socialWindow = new Window(['FB', 'Gitter', 'Reddit', 'Twitter', 'Medium']); // 社交网站
const videoWindow = new Window(['Netflix', 'YouTube', 'Vimeo', 'Vine']); // 娱乐网站
// 现在执行打开选项卡,关闭选项卡和其他操作
const finalTabs = socialWindow
.tabOpen() // 打开一个新的选项卡,显示猫的图片
.join(videoWindow.tabClose(2)) // 关闭视频窗口的第三个选项卡,并合并
.join(workWindow.tabClose(1).tabOpen());
console.log(finalTabs.tabs);
|
三、使用函数式编程避免变化和副作用
上例的问题出在 tabClose() 函数里的 splice
。
不幸的是,splice 修改了调用它的原始数组,所以第二次调用它时是基于修改后的数组,才给出了意料之外的结果。
这是一个小例子,还有更广义的定义 —— 在变量,数组或对象上调用一个函数,这个函数会改变对象中的变量或其他东西。
函数式编程 的核心原则之一是不改变任何东西。
变化会导致错误。
如果一个函数不改变传入的参数、全局变量等数据,那么它造成问题的可能性就会小很多。
前面的例子没有任何复杂的操作,但是 splice 方法改变了原始数组,导致 bug 产生。
在函数式编程中,改变或变更叫做 mutation
,这种改变的结果叫做副作用(side effect)
。
理想情况下,函数应该是不会产生任何副作用的 纯函数(pure function)
。
尝试掌握这个原则:不要改变代码中的任何变量或对象。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 全局变量
let fixedValue = 4;
function incrementer() {
return fixedValue + 1;
};
console.log(incrementer()); // 5
console.log(fixedValue); // 4
|
四、传递参数以避免函数中的外部依赖
上例更接近 函数式编程 原则,但是仍然缺少一些东西。
虽然没有改变全局变量值,但在没有全局变量 fixedValue 的情况下,incrementer 函数将不起作用。
函数式编程的另一个原则是:总是显式声明依赖关系
。
如果函数依赖于一个变量或对象,那么将该变量或对象作为参数直接传递到函数中。
这样做会有很多好处。
-
其中一点是让函数更容易测试,因为确切地知道参数是什么,并且这个参数也不依赖于程序中的任何其他内容。
-
其次,这样做可以更加自信地更改,删除或添加新代码。 因为很清楚哪些是可以改的,哪些是不可以改的,这样就知道哪里可能会有潜在的陷阱。
-
最后,无论代码的哪一部分执行它,函数总是会为同一组输入生成相同的输出。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 全局变量
let fixedValue = 4;
function incrementer(value) {
return value + 1
}
console.log(incrementer(fixedValue)); // 5
console.log(fixedValue); // 4
|
五、在函数中重构全局变量
目前为止,已经看到了函数式编程的两个原则:
给数字增加 1 不够刺激,可以在处理数组或更复杂的对象时应用这些原则。
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
|
// 全局变量
let bookList = ["西游记", "红楼梦", "金瓶梅", "来生不做中国人"];
/* 这个函数将一本书添加到列表中并返回列表 */
// 新参数应该在 bookName 之前
function add(list, bookName) {
return [...list, bookName];
}
/* 这个函数从列表中删除一本书并返回列表 */
// 新参数应该出现在 bookName 一之前
function remove(list, bookName) {
return list.filter(book => book !== bookName);
}
let newBookList = add(bookList, '寂静岭');
let newerBookList = remove(bookList, '红楼梦');
let newestBookList = remove(add(bookList, '寂静岭'), '红楼梦');
console.log(newBookList); // [ '西游记', '红楼梦', '金瓶梅', '来生不做中国人', '寂静岭' ]
console.log(newerBookList); // [ '西游记', '金瓶梅', '来生不做中国人' ]
console.log(newestBookList); // [ '西游记', '金瓶梅', '来生不做中国人', '寂静岭' ]
console.log(bookList); // [ '西游记', '红楼梦', '金瓶梅', '来生不做中国人' ]
|
知识点
1)功能独立 —— 不依赖于程序的状态(比如可能发生变化的全局变量);
2)纯函数 —— 同一个输入永远能得到同一个输出;
3)有限的副作用 —— 可以严格地限制函数外部对状态的更改