JavaScript 中的函数式编程(译)

在这篇文章中,我们将会学习到声明式, 纯函数, 不变性以及副作用。

什么是函数式编程?

  • 在计算机科学中,函数是编程是一种编程范例或者模式(一种构建计算机程序结构和元素的样式)
  • 函数式编程将计算机运算视为数学概念中函数的计算。
  • 函数式编程避免改变状态以及使用可变的数据。
上面的这些定义来自于维基百科, 在这篇文章中,我们尝试理解FP(函数式编程)的价值和意义。

其他主要的编程规范或模式:

  • 过程式编程
  • 面向对象的编程
  • 元编程
  • 命令式编程
  • 声明式编程
过程式编程 基于程序调用的概念,简单包括计算机将要执行的一系列的计算过程, 在程序执行期间的任何时候,任何被设定的程序都有可能被调用,包括被其他的程序被调用或者自身调用,主要的过程式编程语言有COBOL, BASIC, C, ADA 和 GO面向对象编程 基于对象的概念,对象中包含数据(属性)和程序(方法),这种模式更接近于函数式编程,一些重要的面向对象的语言包括:C++, Java, PHP, C#, Python, Ruby, Swift 等等。元编程 具备将程序视为数据的能力,这意味着程序能够被设计成能够阅读,复制, 分析或者转换为其他程序,甚至在运行的时候修改自身。命令模式 vs 声明模式
  • 命令模式关心描述程序如何运行, 由计算机执行的命令组成
  • 声明模式关心程序能够做什么而无需确定程序应该如何完成。
  • 函数式编程遵循声明模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var books = [
{name:'JavaScript', pages:450},
{name:'Angular', pages:902},
{name:'Node', pages:732}
];
/* Imperative Pattern */
for (var i = 0; i < books.length; i++) {
books[i].lastRead = new Date();
}
/* Declarative Pattern */
books.map((book)=> {
book.lastReadBy = 'me';
return book;
});
console.log(books);
  • 在上面的这一段代码中, 我们对于书籍数组中的每一本书添加了一个新的属性,这个过程通过两种不同的方法执行。
  • 第一段中借用 for 循环,依据数组的长度进行迭代,接着将数组的指针计数器和数组的长度进行核对并且在每一次迭代中增加指针计数器,因此, 这更像程序/代码正在关心为了得到想要的输出结果如何进行运行操作。
  • 第二段中的代码借助于原生Js数组中的 map 这个 map 方法将函数作为参数,这个函数获取到每一个元素,因此在这种情况下代码不是在描述程序如何运行,而是讨论需要达成什么 ,在这种情况后的 map 方法 关心实际的程序执行。

数学意义上的函数或者纯函数

在数学中,函数是一系列输入值和合法的输出值之间的关系,这种特性反映了每一个输入组合都关联着一个确定的输出。这函数式编程中,这种函数被称作纯函数,函数的输出结果仅仅取决于函数接收到的输入数据, 除了返回值之外, 函数不会改变输入的数据。Math.random() 不是纯函数,因为每次调用的时候总会返回一个新的值。Math.min(1, 2) 是纯函数的一个例子,对于相同的一组输入值总会返回新的值。

为什么要使用函数式编程

  • 函数式编程中的纯函数确保了不会改变在其范围之外的数据。
  • 其减少了复杂程度, 我们不需要关心程序如何怎样做, 而只需要关心程序做了什么。
  • 易于测试,因为其不会取决于应用的状态,对于结果的验证也将会变得简单。
  • 让代码更具有易读性。
  • 函数式编程让代码更易于理解。

函数式编程的例子

数组函数在上面的代码中, 我们试图过滤出只有活跃的 meet-ups, 我们可以看到这个功能可以使用两种不同的方法实现,在这里, 第二种方法是函数式编程,其中的 filter() 方法关心"程序如何运行",程序只关心输入也就是 meetups数组以及输出activeMeetupsFP 但是在第一种方法中程序也关心 for 循环如何运行代码。相似的,下面的这些数组方法有助于实现函数式编程,减少代码的复杂度。
  • find
  • reduce
  • map
  • some
  • every

函数链

函数链是用于调用多种方法的机制, 每一个方法返回一个对象, 允许在一个声明中调用链接在一起而无需变量来储存中间结果。在上面的代码片段中, 我们想要打印出所有的活跃的 meetup 用户的总人数, 考虑到可能有10%的用户重复。

支持 FP 的库

下面这些库中提供了一些让代码看起来更加声明式的有用函数。
  • RamadaJS
  • UnderscoreJS
  • lodash

副作用

函数或者表达式除了返回一个值之外,在下面的几种情况下被认为产生了副作用: 如果其改变了其自身范围之外的程序状态, 或者与其调用的函数或者外部的程序有一个可以观察到的交互。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let meetup = {name:'JS',isActive:true,members:49};
const scheduleMeetup = (date, place) => {
meetup.date = date;
meetup.place = place;
if (meetup.members < 50)
meetup.isActive = false;
}
const publishMeetup = () => {
if (meetup.isActive) {
meetup.publish = true;
}
}
scheduleMeetup('today','Bnagalore');
publishMeetup();
console.log(meetup);
上面的这段代码产生了副作用,因为函数 scheduleMeetup 的本来的作用是给 meetup 添加 date 和 place, 但是这个函数改变了 isActive 的值, 而这个 isActive 正是函数 publishMeetup 所依赖的。具有副作用的 publishMeetup 函数将不会得到理想的输出, 因为其输入的值在这个过程中被改变了。 在大型的程序中(真实的程序情况下), 很难去 debug 副作用。副作用不总是产生坏的影响,但是如果其产生的时候我们应该小心对待。

不变性

在函数运行之后,不变性是十分重要的对于确保一个函数确实没有改变原来的数据而不是返回数据的新的副本。例如, 如果数组以及对象在经历过多个函数之后, 如果我们不能保持不变性, 那么函数可能不会得到数组或者对象的原始值。在可变的对象和数组的情况下,如果发生了一些错误对于我们来说是非常困难排除bug的。

支持不变性的库

JavaScript默认没有对于使得对象或者数组不变提供任何的工具, 下面是一些可能帮助我们实现不变性的库:
  • Seamless-immutable
  • Immutable JS

总结

函数式编程中主要的方面是纯函数和更小的功能, 函数不变性以及更少的副作用。本文翻译至Functional Programming in JavaScript,实际上翻译之后才发现这里只是一些函数式编程基础知识,对于其他的函数式编程的更多特性并没有涉及,本文仅仅作为函数式编程的基本入门知识, 如果能帮到读者,那就再好不过了。