函数式编程(一)

311 阅读2分钟

为什么要学习函数式编程?

函数式编程是一种方案简单、功能独立、对作用域外没有任何副作用的编程范式:INPUT -> PROCESS -> OUTPUT

函数式编程:

1)功能独立——不依赖于程序的状态(比如可能发生变化的全局变量);

2)纯函数——同一个输入永远能得到同一个输出;

3)有限的副作用——可以严格地限制函数外部对状态的更改。


在代码编辑器中,已经为你定义好了prepareTeagetTea函数。 调用 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

在函数中重构全局变量

目前为止,我们已经看到了函数式编程的两个原则:

  1. 不要更改变量或对象 - 创建新变量和对象,并在需要时从函数返回它们。 提示:使用类似 const newArr = arrVar 的东西其中 arrVar 是一个数组,只会创建对现有变量的引用,而不是副本。 所以更改 newArr 中的值会同时更改 arrVar 中的值
  2. 声明函数参数 - 函数内的任何计算仅取决于参数,而不取决于任何全局对象或变量。

给数字增加 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 参数之前添加。

小结:

目前为止,我们已经看到了函数式编程的两个原则:

  1. 不要更改变量或对象 - 创建新变量和对象,并在需要时从函数返回它们。 提示:使用类似 const newArr = arrVar 的东西其中 arrVar 是个数组,只会创建对现有变量的引用,而不是副本。 所以更改 newArr 中的值会同时更改 arrVar 中的值
  2. 声明函数参数 - 函数内的任何计算仅取决于参数,而不取决于任何全局对象或变量