本文将简要介绍函数式编程的概念,并阐述五种提升 JavaScript 代码函数式风格的方法。
核心要点
const
声明变量来实现。for
循环,因为它们依赖于可变状态。应改用递归和高阶数组方法。此外,应避免类型强制以保持类型一致性。这可以通过在声明函数之前编写类型声明注释来实现。什么是函数式编程?
函数式编程是一种编程范式,它使用函数及其应用,而不是在命令式编程语言中使用的命令列表。
这是一种更抽象的编程风格,其根源在于数学——特别是称为 lambda 演算的数学分支,该分支由数学家 Alonzo Church 于 1936 年设计为可计算性的形式模型。它由表达式和函数组成,这些表达式和函数将一个表达式映射到另一个表达式。从根本上说,这就是我们在函数式编程中所做的:我们使用函数将值转换为不同的值。
本文作者近年来爱上了函数式编程。我们开始使用鼓励更函数式风格的 JavaScript 库,然后通过学习如何在 Haskell 中编写代码直接跳入深水区。
Haskell 是一种纯函数式编程语言,开发于 20 世纪 90 年代,类似于 Scala 和 Clojure。使用这些语言,您将被迫采用函数式风格进行编码。学习 Haskell 使我们真正了解了函数式编程提供的所有优势。
JavaScript 是一种多范式语言,因为它可以用于以命令式、面向对象或函数式风格进行编程。但是,它确实特别适合函数式风格,因为函数是第一类对象,这意味着它们可以赋值给变量。这也意味着函数可以作为参数传递给其他函数(通常称为回调),也可以作为其他函数的返回值。返回其他函数或接受其他函数作为参数的函数称为高阶函数,它们是函数式编程的基础部分。
近年来,以函数式风格编程 JavaScript 变得越来越流行,尤其是在 React 兴起之后。React 使用适合函数式方法的声明式 API,因此扎实掌握函数式编程原理将改进您的 React 代码。
为什么函数式编程如此优秀?
简而言之,函数式编程语言通常会导致代码简洁、清晰且优雅。代码通常更容易测试,并且可以在多线程环境中运行而不会出现任何问题。
如果您与许多不同的程序员交谈,您可能会从每个人那里获得关于函数式编程的完全不同的意见——从那些绝对讨厌它的人到那些绝对喜欢它的人。我们(本文作者)站在“喜欢它”的一端,但我们完全理解它并非每个人的菜,尤其因为它与通常教授的编程方式大相径庭。
但是,一旦您掌握了函数式编程,并且一旦思维过程点击到位,它就会成为第二天性,并改变您编写代码的方式。
规则 1:净化您的函数
函数式编程的关键部分是确保您编写的函数是“纯”的。如果您不熟悉这个术语,纯函数基本上满足以下条件:
纯函数必须至少有一个参数,并且必须返回值。如果您仔细考虑,如果它们不接受任何参数,它们将没有任何数据可以使用,如果它们不返回值,那么函数的意义何在?
纯函数一开始可能看起来并非完全必要,但使用不纯函数会导致程序发生整体变化,从而导致一些严重的逻辑错误!
例如:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
在不纯函数中,checkAge
函数依赖于可变变量 minimum
。例如,如果稍后在程序中更新 minimum
变量,则 checkAge
函数可能会使用相同的输入返回布尔值。
假设我们运行以下代码:
<code class="language-javascript">checkAge(20); // false</code>
现在,假设稍后在代码中,changeToUK()
函数将 minimum
的值更新为 18。
然后,假设我们运行以下代码:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
现在,checkAge
函数评估为不同的值,尽管给出了相同的输入。
纯函数使您的代码更易于移植,因为它们不依赖于作为参数提供的任何值之外的任何其他值。返回值永远不会改变的事实使纯函数更容易测试。
一致地编写纯函数还可以消除发生突变和副作用的可能性。
突变是函数式编程中的一大危险信号,如果您想了解更多信息,可以阅读关于它们在《JavaScript 中变量赋值和突变指南》中的内容。
为了使您的函数更易于移植,请确保您的函数始终保持纯净。
规则 2:保持变量不变
声明变量是任何程序员学习的第一件事之一。它变得微不足道,但在使用函数式编程风格时却极其重要。
函数式编程的关键原则之一是,一旦设置了变量,它就会在整个程序中保持该状态。
这是显示代码中变量的重新赋值/重新声明如何成为灾难的最简单的示例:
<code class="language-javascript">checkAge(20); // false</code>
如果您仔细考虑,n
的值不可能同时为 10 和 11;这在逻辑上说不通。
命令式编程中的一种常见编码实践是使用以下代码递增值:
<code class="language-javascript">checkAge(20); // true</code>
在数学中,语句 x = x 1
是不合逻辑的,因为如果您从两边减去 x
,您将得到 0 = 1
,这显然是不正确的。
因此,在 Haskell 中,您不能将变量赋值给一个值,然后将其重新赋值给另一个值。为了在 JavaScript 中实现这一点,您应该遵循始终使用 const
声明变量的规则。
规则 3:使用箭头函数
在数学中,函数的概念是将一组值映射到另一组值的概念。下图显示了通过平方将左侧的值集映射到右侧的值集的函数:
这就是使用箭头表示法在数学中编写它的方式:f: x → x²。这意味着函数 f 将值 x 映射到 x²。
我们可以使用箭头函数几乎以相同的方式编写此函数:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
在 JavaScript 中使用函数式风格的一个关键特征是使用箭头函数而不是常规函数。当然,这确实归结为风格,使用箭头函数而不是常规函数实际上不会影响代码的“函数式”程度。
但是,在使用函数式编程风格时,最难适应的事情之一是将每个函数都视为输入到输出的映射的思维方式。没有所谓的过程。我们发现使用箭头函数可以帮助我们更好地理解函数的过程。
箭头函数具有隐式返回值,这确实有助于可视化此映射。
箭头函数的结构——尤其是它们的隐式返回值——有助于鼓励编写纯函数,因为它们的结构实际上是“输入映射到输出”:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
另一件事是我们喜欢强调的,尤其是在编写箭头函数时,是使用三元运算符。如果您不熟悉三元运算符,它们是内联 if...else
语句,形式为 condition ? value if true : value if false
。
您可以阅读更多关于它们在《快速提示:如何在 JavaScript 中使用三元运算符》中的内容。
在函数式编程中使用三元运算符的主要原因之一是 else
语句的必要性。程序必须知道如果不满足原始条件该怎么做。例如,Haskell 会强制执行 else
语句,如果没有给出 else
语句,它将返回错误。
使用三元运算符的另一个原因是它们是始终返回值的表达式,而不是可以用于执行可能具有副作用的操作的 if-else
语句。这对于箭头函数特别有用,因为它意味着您可以确保存在返回值并保持输入到输出映射的图像。如果您不确定语句和表达式之间的细微差别,那么关于语句与表达式的指南非常值得一读。
为了说明这两个条件,以下是一个使用三元运算符的简单箭头函数示例:
<code class="language-javascript">checkAge(20); // false</code>
action
函数将根据 state
参数的值返回“eat”或“sleep”的值。
因此,总之:在使您的代码更具函数式时,您应该遵循以下两条规则:
if...else
语句替换为三元运算符规则 4:删除 for 循环
鉴于使用 for
循环编写迭代代码在编程中非常常见,说要避免它们似乎很奇怪。事实上,当我们第一次发现 Haskell 甚至没有任何类型的 for
循环操作时,我们难以理解如何实现某些标准操作。但是,有一些非常好的理由说明为什么 for
循环不会出现在函数式编程中,我们很快发现每种类型的迭代过程都可以在不使用 for
循环的情况下实现。
不使用 for
循环最重要的原因是它们依赖于可变状态。让我们来看一个简单的求和函数:
<code class="language-javascript">checkAge(20); // true</code>
如您所见,我们必须在 for
循环本身以及我们在 for
循环中更新的变量中使用 let
。
如前所述,这通常是函数式编程中的不良做法,因为函数式编程中的所有变量都应该是不可变的。
如果我们想编写所有变量都是不可变的代码,我们可以使用递归:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
如您所见,没有变量被更新。
我们当中的数学家显然会知道所有这些代码都是不必要的,因为我们可以只使用巧妙的求和公式 0.5*n*(n 1)。但这是一种很好的方法,可以说明 for
循环的可变性与递归之间的区别。
但是,递归并不是解决可变性问题的唯一解决方案,尤其是在处理数组时。JavaScript 具有许多内置的高阶数组方法,这些方法可以遍历数组中的值而无需更改任何变量。
例如,假设我们要将 1 加到数组中的每个值。使用命令式方法和 for
循环,我们的函数可能如下所示:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
但是,我们可以使用 JavaScript 的内置 map
方法并编写如下所示的函数:
<code class="language-javascript">checkAge(20); // false</code>
如果您以前从未见过 map
函数,那么绝对值得了解它们——以及 JavaScript 的所有内置高阶数组方法,例如 filter
,尤其如果您真的对 JavaScript 中的函数式编程感兴趣。您可以在《不可变数组方法:如何编写非常清晰的 JavaScript 代码》中找到更多关于它们的信息。
Haskell 完全没有 for
循环。为了使您的 JavaScript 更具函数式,请尝试通过使用递归和内置的高阶数组方法来避免使用 for
循环。
规则 5:避免类型强制
在使用 JavaScript 等不需要类型声明的语言进行编程时,很容易忘记数据类型的重要性。JavaScript 中使用的七种原始数据类型是:
Haskell 是一种强类型语言,需要类型声明。这意味着在任何函数之前,您都需要使用 Hindley-Milner 系统指定进入的数据类型和输出的数据类型。
例如:
<code class="language-javascript">checkAge(20); // true</code>
这是一个非常简单的函数,它将两个数字(x 和 y)加在一起。对于每个函数,包括像这样的非常简单的函数,都必须向程序解释数据类型似乎有点荒谬,但这最终有助于显示函数的预期工作方式及其预期返回的内容。这使得代码更容易调试,尤其是在代码变得更复杂时。
类型声明遵循以下结构:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
类型强制在使用 JavaScript 时可能是一个大问题,JavaScript 具有各种可以用来(甚至滥用)绕过数据类型不一致的技巧。以下是最常见的技巧以及如何避免它们:
if
语句中的 0 值等效于 false。这可能导致懒惰的编程技术,忽略检查数值数据是否等于 0。例如:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
这是一个评估数字是否为偶数的函数。它使用 !
符号将 n % 2
的结果强制转换为布尔值,但 n % 2
的结果不是布尔值,而是一个数字(0 或 1)。
像这样的技巧,虽然看起来很聪明,并且减少了您编写的代码量,但它们破坏了函数式编程的类型一致性规则。因此,编写此函数的最佳方法如下所示:
<code class="language-javascript">checkAge(20); // false</code>
另一个重要概念是确保数组中的所有数据值都是相同类型。JavaScript 不会强制执行此操作,但如果没有相同类型,当您想要使用高阶数组方法时可能会导致问题。
例如,一个将数组中所有数字相乘并返回结果的乘积函数可以用以下类型声明注释编写:
<code class="language-javascript">checkAge(20); // true</code>
在这里,类型声明清楚地表明函数的输入是一个包含 Number 类型元素的数组,但它只返回一个数字。类型声明清楚地说明了此函数的预期输入和输出。显然,如果数组不只包含数字,则此函数将无法工作。
Haskell 是一种强类型语言,而 JavaScript 是一种弱类型语言,但为了使您的 JavaScript 更具函数式,您应该在声明函数之前编写类型声明注释,并确保避免类型强制快捷方式。
我们还应该在这里提到,如果您想要 JavaScript 的强类型替代方案,该替代方案将为您强制执行类型一致性,那么您可以转向 TypeScript。
结论
总而言之,以下五个规则将帮助您实现函数式代码:
for
循环。虽然这些规则不能保证您的代码是纯函数式的,但它们将在很大程度上使其更具函数式,并有助于使其更简洁、更清晰且更容易测试。
我们真的希望这些规则能像帮助我们一样帮助您!我们两人都是函数式编程的忠实粉丝,我们强烈建议任何程序员使用它。
如果您想进一步深入研究函数式 JavaScript,我们强烈建议您阅读《弗里斯比教授关于函数式编程的大部分充分指南》,该指南可以在线免费获取。如果您想全力以赴学习 Haskell,我们建议您使用 Try Haskell 交互式教程并阅读优秀的《学习 Haskell 以获得更大的好处》一书,该书也可以在线免费阅读。
关于 JavaScript 函数式编程的常见问题
什么是 JavaScript 中的函数式编程? 函数式编程是一种编程范式,它将计算视为数学函数的评估,并避免更改状态和可变数据。在 JavaScript 中,它涉及使用函数作为一等公民并避免副作用。
什么是 JavaScript 中的一等函数? JavaScript 中的一等函数意味着函数被视为与任何其他变量一样。它们可以赋值给变量,作为参数传递给其他函数,并作为值从其他函数返回。
什么是函数式编程中的不变性? 不变性是指一旦创建对象,就不能更改它。在 JavaScript 函数式编程的上下文中,这意味着在初始化变量或数据结构后避免修改它们。
什么是高阶函数? 高阶函数是将其他函数作为参数或返回函数作为结果的函数。它们支持函数的组合,使创建模块化和可重用代码更容易。
是否有任何库/框架可以促进 JavaScript 中的函数式编程? 是的,一些库和框架(例如 Ramda 和 lodash)提供了支持 JavaScript 函数式编程概念的实用程序和函数。它们可以帮助简化和增强函数式编程实践。
以上是使您的JavaScript更具功能性的5种方法的详细内容。更多信息请关注PHP中文网其他相关文章!