(译) 函数式 JS #2: 函数!

661 阅读6分钟

close-up photo of factory
"close-up photo of factory" by Taton Moïse on Unsplash

原文链接 By: Krzysztof Czernek

这是 "函数式 JS" 系列的第二篇。查看第一篇

简介

现在我们知道为什么学习函数式编程实践可以帮助你成为一个更好的程序员了,那让我们开始一些有趣的东西吧。

在这一部分中,我们将重点关注与函数式编程相关的术语基本概念

遗憾的是,这次不会涉及很多代码。从好的方面来说,一旦我们理解了术语,我们就能够方便地讨论更复杂的主题。


函数

毋庸置疑,函数式编程中最重要的就是函数。

我们都知道什么是函数。它基本上就是一个(大多数时候都会有一个名字的)代码片段。

function add (x, y) {
  return x + y
}

然而,当谈到函数式编程时,我们更想从一个特殊的角度来看待函数:数学中的函数。

数学?没搞错吧...

即使我们不必真的去使用代数,我们也不得不承认函数式编程深深植根于数学。

在简单的数学术语中,函数是一种在给定输入的情况下产生特定输出的机器

有趣的是,给定输入只能有一个输出。这意味着,如果我们为函数提供相同的输入,我们希望它始终做同样的事情,并且返回相同的值

这听起来没什么大不了,但实际上这是一个很强的限制。这个数学定义有着深远的影响:

  • 除了输入(参数)之外,函数不能有其他任何依赖
  • 函数必须返回单个值
  • 函数必须是确定性的(不能使用随机值等)

满足这些标准的函数在编程中称为纯函数,它们对于函数式范式至关重要。

纯函数

让我们看一下 JavaScript 中的一些函数示例,直接体会一下什么是纯函数。

function coin () {
  return Math.random() < 0.5 ? 'heads' : 'tails'
}

Coin 不是纯函数,因为它在给定相同输入(null)的情况下并不总是产生相同的结果 - 它不是确定性的。

let firstName = 'krzysztof'

function uppercaseName (lastName) {
  return `${firstName.toUpperCase()} ${lastName.toUpperCase()}`
}

uppercaseName 不是纯函数,因为它依赖于一个不受其控制的变量。我们无法确定在给定相同参数的情况下它总会产生相同的结果。

let user = {
  firstName: 'Krzysztof',
  age: '26'
}

function happyBirthday () {
  user.age = user.age + 1
}

happyBirthday不是纯函数,因为它不仅访问了一个不受控制的变量,还不会返回任何内容。

function calculatePrice (unitPrice, noOfUnits, couponValue = 0) {
  return unitPrice * noOfUnits - couponValue;
}

calculatePrice是纯函数。它不使用任何超出其控制范围的变量,它是确定性的,我们可以非常有信心地说它将始终为相同的输入参数组合返回相同的结果。

然后呢?

为什么这一切很重要?有以下一些原因表明纯函数比非纯函数更有优势:

  1. 更易阅读

你只需要读一下它的函数体就知道它做了哪些事情。

  1. 更易理解

不需要查找外部依赖,函数被调用的上下文等。这些对于纯函数都没有任何影响。

  1. 更易测试

如果你想测试一个纯函数,你只需要用一些参数调用它,看看结果是否是你想要的结果。根本无需复杂的设置。

  1. 更高效

如果我们知道对于给定的输入,函数将始终产生相同的输出,我们就可以缓存(memoize)它的结果,这样我们就不必在每次调用这个函数的时候都重新计算它。

使用纯函数可以使代码更易于维护 - 因为它可以更轻松地管理副作用。在接下来的部分中,我们将了解副作用是什么以及为什么,遗憾的是,计算机程序中不可能全部都是纯函数。

现在我们知道了什么是纯函数,让我们关注下一个与函数相关的术语:作为一等公民的函数

一等公民函数

与“纯函数”不同,“一等公民函数”在日常工作中并不是一个很实用的概念。但是,在考虑编程语言的特性时,它就很有用了。

如果在一个编程语言中,函数可以像使用其他的值同样的方式使用,那么你就可以说这个语言具有“一等公民的函数”,也就是说:

  1. 它们可以被传递,
  2. 它们可以被分配给变量,
  3. 它们可以被存储在更复杂的数据结构中,如数组或对象。

可以说没有一等公民的函数,就没有函数式编程(至少会非常的尴尬)。下面这个例子,说明为什么函数在 JavaScript 中是一等公民:

function add (a, b) {
  return a + b
}

function multiply (a, b) {
  return a * b
}

const operations = { // 这里我们把函数当成普通的值使用
  add,
  multiply
}

operations.add(1, 2)

正如上面所说,JavaScript 的函数可以在不同的函数之间传递。但是......这么做的目的是什么呢?

嗯,将函数传入和传出到另外一个函数是函数式编程中的常见做法 - 而且功能非常强大。它给我们引入了...

高阶函数

可以“操作”其他函数的函数被称为高阶函数。这里的操作,意思是指他们可以做到下面两点中的一个或两个:

  • 把其他函数作为参数,
  • 返回一个函数。

这个例子在 JavaScript 世界中很常见。其中一个示例是标准库中的 Array.prototype.map 函数。它需要一个函数作为参数并将其应用于数组中的每个元素:

const numbers = [1, 1, 2, 3, 5, 8]
const transformFunction = x => x + 2

numbers.map(transformFunction)

下面是一个返回函数的函数,这个示例稍显刻意:

function makeGreeter (greeting) {
  return function greet (name) {
    return `${greeting}, ${name}!`
  }
}

// 或者使用 ES6 的语法:

const makeGreeter = greeting => name => `${greeting}, ${name}!`

const greet = makeGreeter('Hello')
console.log(greet('Krzysztof'))

你可以看到,这些函数(map 和 makeGreeter)不接受或者返回我们所知道的那些常规的值。他们在操作函数。

你可能已经熟悉了一些高阶函数,例如:

  • map,
  • reduce,
  • filter,
  • compose,
  • forEach,
  • … 和别的。

函数式编程就是将一些小型,可重用和通用的函数组合成更复杂的函数。因此,在后面的讨论中你将会看到更多不同的高阶函数。


那么,这就是我们开始 FP 之旅所需的所有与函数相关的基本术语了。

下一章,我们将关注函数式编程中的状态 (state) - 如何管理它,以及如何避免它带来的问题等等。我们已经提到过一些关于状态的内容(在讨论纯函数的时候),后面还有更多!

我们已经学到了不少东西,希望你和我一样对下一章感到兴奋!