为什么要学习函数式编程?
函数式编程是一种方案简单、功能独立、对作用域外没有任何副作用的编程范式:INPUT -> PROCESS -> OUTPUT。
函数式编程:
1)功能独立——不依赖于程序的状态(比如可能发生变化的全局变量);
2)纯函数——同一个输入永远能得到同一个输出;
3)有限的副作用——可以严格地限制函数外部对状态的更改。
在代码编辑器中,已经为你定义好了prepareTea和getTea函数。 调用 getTea 函数为团队准备 40 杯茶,并将它们存储在 tea4TeamFCC 变量里
// 函数返回表示“一杯绿茶(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(40) ;
了解函数式编程术语
Callbacks 是被传递到另一个函数中调用的函数。 你应该已经在其他函数中看过这个写法,例如在 filter 中,回调函数告诉 JavaScript 以什么规则过滤数组。
函数就像其他正常值一样,可以赋值给变量、传递给另一个函数,或从其它函数返回,这种函数叫做头等 first class 函数。 在 JavaScript 中,所有函数都是头等函数。
将函数为参数或返回值的函数叫做高阶 ( higher order) 函数。
当函数被传递给另一个函数或从另一个函数返回时,那些传入或返回的函数可以叫做 lambda。
// 函数返回表示“一杯绿茶(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, 27);
const tea4BlackTeamFCC = getTea(prepareBlackTea,13);
console.log(
tea4GreenTeamFCC,
tea4BlackTeamFCC
);
了解使用命令式编程的危害
使用函数式编程是一个好的习惯。 它使你的代码易于管理,避免潜在的 bug。 但在开始之前,先看看命令式编程方法,以强调你可能有什么问题。
在英语 (以及许多其他语言) 中,命令式时态用来发出指令。 同样,命令式编程是向计算机提供一套执行任务的声明。
命令式编程常常改变程序状态,例如更新全局变量。 一个典型的例子是编写 for 循环,它为一个数组的索引提供了准确的迭代方向。
相反,函数式编程是声明式编程的一种形式。 通过调用方法或函数来告诉计算机要做什么。
JavaScript 提供了许多处理常见任务的方法,所以你无需写出计算机应如何执行它们。 例如,你可以用 map 函数替代上面提到的 for 循环来处理数组迭代。 这有助于避免语义错误,如调试章节介绍的 "Off By One Errors"。
考虑这样的场景:你正在浏览器中浏览网页,并想操作你打开的标签。 下面我们来试试用面向对象的思路来描述这种情景。
窗口对象由选项卡组成,通常会打开多个窗口。 窗口对象中每个打开网站的标题都保存在一个数组中。 在对浏览器进行了如打开新标签、合并窗口、关闭标签之类的操作后,你需要输出所有打开的标签。 关掉的标签将从数组中删除,新打开的标签(为简单起见)则添加到数组的末尾。
代码编辑器中显示了此功能的实现,其中包含 tabOpen(),tabClose(),和 join() 函数。 tabs 数组是窗口对象的一部分用于储存打开页面的名称。
在编辑器中运行代码。 它使用了有副作用的方法,导致输出错误。 存储在 finalTabs.tabs 中的打开标签的最终列表应该是 ['FB', 'Gitter', 'Reddit', 'Twitter', 'Medium', 'new tab', 'Netflix', 'YouTube', 'Vine', 'GMail', 'Work mail', 'Docs', 'freeCodeCamp', 'new tab'],但输出会略有不同。
修改 Window.prototype.tabClose 使其删除正确的标签。
// 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) {
// 只修改这一行下面的代码
const tabsBeforeIndex = this.tabs.splice(0, index); // 点击之前获取 tabs
const tabsAfterIndex = this.tabs.slpice(index + 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()改成slice()即可。
这是一个小例子,还有更广义的定义——在变量,数组或对象上调用一个函数,这个函数会改变对象中的变量或其他东西。
函数式编程的核心原则之一是不改变任何东西。 变化会导致错误。 如果一个函数不改变传入的参数、全局变量等数据,那么它造成问题的可能性就会小很多。
前面的例子没有任何复杂的操作,但是 splice 方法改变了原始数组,导致 bug 产生。
回想一下,在函数式编程中,改变或变更叫做 mutation,这种改变的结果叫做“副作用”(side effect)。 理想情况下,函数应该是不会产生任何副作用的 pure function。
让我们尝试掌握这个原则:不要改变代码中的任何变量或对象。
填写 incrementer函数的代码,使其返回值为全局变量 fixedValue增加 1 。
// 全局变量
var fixedValue = 4;
function incrementer() {
return fixedValue + 1;
}
var newValue = incrementer(); // 5
console.log(fixedValue); //4
传递参数以避免函数中的外部依赖
上一个挑战是更接近函数式编程原则的挑战,但是仍然缺少一些东西。
虽然我们没有改变全局变量值,但在没有全局变量 fixedValue 的情况下,incrementer 函数将不起作用。
函数式编程的另一个原则是:总是显式声明依赖关系。 如果函数依赖于一个变量或对象,那么将该变量或对象作为参数直接传递到函数中。
这样做会有很多好处。 其中一点是让函数更容易测试,因为你确切地知道参数是什么,并且这个参数也不依赖于程序中的任何其他内容。
其次,这样做可以让你更加自信地更改,删除或添加新代码。 因为你很清楚哪些是可以改的,哪些是不可以改的,这样你就知道哪里可能会有潜在的陷阱。
最后,无论代码的哪一部分执行它,函数总是会为同一组输入生成相同的输出。
更新 incrementer 函数,明确声明其依赖项。
编写 incrementer 函数,获取它的参数,然后将值增加 1。
var fixedValue = 4;
function incrementer(value) {
return value + 1;
}
var differentValue = incrementer(fixedValue); // 5
console.log(fixedValue); // 4
在函数中重构全局变量
目前为止,我们已经看到了函数式编程的两个原则:
- 不要更改变量或对象 - 创建新变量和对象,并在需要时从函数返回它们。 提示:使用类似
const newArr = arrVar的东西,其中arrVar是一个数组,只会创建对现有变量的引用,而不是副本。 所以更改newArr中的值会同时更改arrVar中的值。 - 声明函数参数 - 函数内的任何计算仅取决于参数,而不取决于任何全局对象或变量。
给数字增加 1 不够刺激,我们可以在处理数组或更复杂的对象时应用这些原则。
// the global variable
var bookList = ["The Hound of the Baskervilles", "On The Electrodynamics of Moving Bodies", "Philosophiæ Naturalis Principia Mathematica", "Disquisitiones Arithmeticae"];
function add(arr, bookName) {
let newArr = [...arr]; // Copy the bookList array to a new array.
newArr.push(bookName); // Add bookName parameter to the end of the new array.
return newArr; // Return the new array.
}
function remove(arr, bookName) {
let newArr = [...arr]; // Copy the bookList array to a new array.
if (newArr.indexOf(bookName) >= 0) {
// Check whether the bookName parameter is in new array.
newArr.splice(newArr.indexOf(bookName), 1); // Remove the given paramater from the new array.
return newArr; // Return the new array.
}
}
var newBookList = add(bookList, 'A Brief History of Time');
var newerBookList = remove(bookList, 'On The Electrodynamics of Moving Bodies');
var newestBookList = remove(add(bookList, 'A Brief History of Time'), 'On The Electrodynamics of Moving Bodies');
console.log(bookList);
重构代码,使全局数组 bookList 在函数内部不会被改变。 add 函数可以将指定的 bookName 增加到数组末尾并返回一个新的数组(列表)。 remove 函数可以从数组中移除指定 bookName。
注意: 两个函数都应该返回一个数组,任何新参数都应该在 bookName 参数之前添加。
小结:
目前为止,我们已经看到了函数式编程的两个原则:
- 不要更改变量或对象 - 创建新变量和对象,并在需要时从函数返回它们。 提示:使用类似
const newArr = arrVar的东西,其中 arrVar 是个数组,只会创建对现有变量的引用,而不是副本。 所以更改 newArr 中的值会同时更改 arrVar 中的值。 - 声明函数参数 - 函数内的任何计算仅取决于参数,而不取决于任何全局对象或变量。