现代 JavaScript 中的函数式编程范例:纯函数
现代 JavaScript 中的函数式编程范例:纯函数

JavaScript 是最流行的编程语言之一。它可以在浏览器、桌面、移动设备甚至冰箱上运行。它背后还有一个最具活力的生态系统和充满激情的社区,这意味着你将永远有新的东西可以学习。
ES6 的引入可能是这种语言有史以来最大的更新——lambdas、类、生成器、析构、更好的语法等等。Async-await 是另一个重要的附加功能,它消除了基于回调或承诺的异步请求的负担。如果语言中没有内置该功能,那么您正在寻找的功能可能已经存在于库中。总而言之,JavaScript 开发者并不缺乏信息和知识。
最近在 JS 社区中流行的一种编程范例是函数式编程。如果你在过去一年浏览过 medium,你肯定会看到关于这个话题的帖子。
繁荣还是萧条?
什么是函数式编程?从哪里来,谁用?对于一些开发人员来说,这听起来像是一个深奥的术语或遥远的东西,他们没有理由投入时间。当世界完全接受面向对象编程时,你为什么要花时间去学习一种完全不同的构建程序的思维方式呢?
虽然这个世界仍然是面向对象的,但是学习新的东西总是对你的技能有好处的。作为开发人员,我们在不断学习,但是为了努力向前,你有时需要忘记一些概念。
新的编程语言正在崛起,其中许多都采用了不同的函数式方法——Scala、Elixir、Elm。如果你是一个活跃的 JavaScript 开发者,你已经在你的应用中使用 FP 方法很长时间了。
本系列文章的目的是阐明如何将函数式编程的不同概念付诸实践,以及这对您有什么好处。虽然许多教程坚持使用你永远也不会用到的算术运算的一般例子,但在这里,我将试着向你展示如何把你新发现的函数技巧集成到你此时正在编写的程序中。
函数式编程&纯函数
函数式编程的核心思想是程序由一组函数组成,它们遵循一定的规则。这里没有类,没有继承,你在这里会遇到很多不同的模式。
FP 的主要概念是纯函数的思想。这些函数接受一些输入,对其进行操作,并返回一个输出,而不修改函数范围之外的任何变量。因此,每个使用 DOM 或使用不在其范围内的变量的函数都被认为是不纯的,不符合函数式编程的标准。
但是一个程序怎么能完全由纯函数组成呢?没有,当然没有。如果你有一个纯函数的程序,它可能不会做任何令人兴奋的事情。您需要能够使用 DOM,您需要能够向服务发送请求或在控制台中记录消息。FP 的目标是将你程序的大部分由小块逻辑组成,这些小块逻辑可以组合在一起并重用。副作用是不可避免的,但是通过将它们限制在应用程序中的某些地方,它们将更容易管理和跟踪。
可预测性
编写小型纯函数的主要好处可能是它们的可预测性。由于你的函数不触及外部变量,也不修改它范围之外的任何东西,你可以很容易地根据输入判断出它的输出。这实际上是定义纯函数的另一个规则——给定相同的输入,它应该总是返回相同的输出。
如果你以前从未使用过函数式编程,这可能听起来有点违反直觉。您可以将此视为在代码库中强制实施关注点分离的一种非常严格的方式。每一个逻辑,每一个函数都应该做一件事情,并且不应该干扰代码的其他部分的工作。
这是一个纯函数的例子,我在自己编写的一个应用程序中使用它作为过滤器。验证函数是否是纯函数的一个简单方法是多次运行它。如果它没有使用全局变量或者在它的作用域之外操作状态,那么无论你调用它多少次,它都应该总是返回相同的输入。
这是一个不纯的函数,你应该尽可能地避免编写这些函数。即使你第一次运行时得到了你想要的结果,任何后续调用都会导致一个NaN。更不用说像这样的东西在任何其他场景或程序的其他部分都是完全不可用的。正如我们将在下一篇文章中看到的,函数式编程的更大目标是能够用许多小的可重用构建块(函数)来组成您的程序。
为了进一步说明您一直是如何处理这种模式的,我们将看看社区最喜欢的库——React。在 react 中,你可以创建无状态的功能组件——不做任何花哨的事情的组件,用于显示 UI 的一部分,而不能访问生命周期方法。这些组件是使用纯函数的概念实现的——它们只是接受一个输入并返回 JSX。
下面是我使用的一个组件的真实例子:
无论你调用它们多少次,它们都会产生相同的结果,返回相同的 JSX。您可以忽略返回的实际结构,但请注意它完全取决于传递给它的输入,包括onVideoSelect函数。这意味着这可以重复用于多个视频,如果我想以不同的方式处理项目上的点击,我只需要传递另一个函数。
在我们讨论 React 主题的同时,让我们更深入地研究一下 Redux。当你想改变 React 应用的状态时,你需要使用名为 reducers 的函数来完成。尽管名字如此,reducer只是简单的函数,它接受先前的状态和您想要进行的更改,并返回新的状态。这听起来确实很纯粹,不是吗?
Reducers 用于描述应用程序状态中的每一个变化,它们以函数的方式来实现这一点,而不需要访问或修改全局变量。它们只对你作为输入传递给它们的东西起作用,因此你知道通过将某些值作为输入传递给 reducer,你的应用程序会以某种方式出现。正如我们将在本文后面看到的,这是非常可预测的,并且易于调试和测试。
独立的
让我们来看看另一个规则,它对于编写合适的纯函数是至关重要的——它们不能修改函数的调用者。这意味着永远不要修改this对象的任何属性。神秘的this是 JavaScript 语言的一个特征,它给全世界的开发者带来了难以置信的悲伤。
每次你依赖this时,你都需要跟踪并注意你的函数是如何以及在哪里被调用的。在函数式编程中,修改函数的调用者是不可接受的,被认为是副作用。
你可能一直都在使用纯函数,但是如果你是这门学科的新手,你可能到现在都没怎么考虑过它。JavaScript 语言的核心中内置了许多遵循该范例的函数,比如 String 对象原型上的函数。
我们正在看的函数是concat。如你所见,它并没有修改被调用的函数。如果您是这门语言的新手,您可能希望 hello 变量的值为“Hello World”。但是,因为 concat 函数遵循 FP 标准,所以执行它的变量保持不变。事实上,如果您想访问 concat 的返回值,您需要将它赋给另一个变量或直接传递给某个变量。
JavaScript 中还有其他函数在函数式编程中被广泛使用。更具体地说,这就是数组原型上的方法——映射、过滤和减少。
与前面的例子非常相似,numbers数组将保持不变。map 函数将返回一个全新的数组。然而,关于这些函数还有一些更有趣的东西——它们是所谓的高阶函数的例子。
高阶函数是可以将一个函数作为输入或返回一个函数作为输出的函数。正如你在例子中看到的,我们不是传递一个值,而是传递一个要在数组的每个元素上执行的函数。
这是需要记住的另外一件事——函数是第一类!它们可以作为输入传递,也可以作为函数的结果返回。这与它们将返回一个全新的数组而不是修改被调用者的事实相结合,允许我们为了更好的效率而链接它们:
我们在这一部分所涵盖的内容暗示了函数式编程的其他部分— 不变性和函数组合。为了不使这篇文章过于复杂,我现在有意跳过它们。
测试
让我们从事物的质量保证方面来看纯粹的功能。我们都知道编写测试是至关重要的,也是非常有益的,但是我们并不总是这样做。通过使用函数方法,你可以避免任何不必要的副作用,并且你的函数不会从全局范围访问或修改任何东西。这消除了复杂的注入和模仿的负担——函数需要的一切都作为输入传递,您只需要验证输出是否正确。
测试你的功能变得简单多了。事实上,你可以把你的测试用例看得更像是一个健全的检查。当你的应用程序的构建模块是独立的,并且不做任何特别的事情时,你只需要确保一切都是它应该的样子。测试变成了一个检查的过程。您有一个问题列表(您的输入参数)和一个答案列表(您的输出参数)-现在您只需要将函数的答案与实际答案进行比较。如果你写了这样的代码,QA 部门的任何人都会非常有帮助。
每一个使用 Redux like 模式的应用程序构建都比那些不使用 Redux like 模式的更容易测试。正如我在本文前面提到的,应用程序状态的每一个变化都被描述为一个纯粹的函数,它有一个基于输入的可预测的输出。这是最简单的测试——我们只需调用带有某个值的函数。
在下面的例子中,我通过传递一个他不应该响应的动作和另一个他应该响应的动作来测试 reducer 的行为。在这两种情况下,具有相同参数的多个调用将提供相同的输出。
下一步是什么?
函数式编程可能是一个令人不知所措和困惑的话题,但是为了开始感到舒服,我们需要对它的组成部分——函数——有一个坚实的理解。
在本文中,我们介绍了在代码库中使用纯函数的应用和好处。
下一篇文章是关于不变性的,你可以在这里阅读。
如果你已经读到这里,真诚地感谢你的阅读。任何反馈都将不胜感激,如果你喜欢你所读到的,请按住鼓掌按钮!