近年来,JavaScript 中的函数式编程越来越流行。虽然它的一些经常宣传的原则(如不变性)需要运行时变通方案,但该语言对函数的一流处理证明了它对由这个基本原语驱动的可组合代码的支持。在介绍如何从其他函数动态组合函数之前,让我们简要回顾一下。
关键要点
什么是函数?
实际上,函数是一个过程,它允许执行一组命令式步骤来执行副作用或返回值。例如:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
当此函数使用拥有 firstName 和 lastName 属性的对象调用时,getFullName 将返回一个包含这两个对应值的字符串:
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
值得注意的是,从 ES2015 开始,JavaScript 现在支持箭头函数语法:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
鉴于我们的 getFullName 函数的元数为 1(即单个参数)并且只有一个 return 语句,我们可以简化此表达式:
const getFullName = person => `${person.firstName} ${person.surname}`;
这三个表达式,尽管方法不同,但最终都达到了相同的目的:
通过返回值组合函数
除了将函数返回值分配给声明(例如 const person = getPerson();)之外,我们还可以使用它们来填充其他函数的参数,或者一般来说,在 JavaScript 允许它们的地方提供值。假设我们有各自执行日志记录和 sessionStorage 副作用的函数:
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
我们可以使用嵌套调用对 getPerson 的返回值执行这些操作:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
鉴于需要根据调用的方式为函数提供所需的参数,因此最内层的函数将首先被调用。因此,在上面的示例中,getPerson 的返回值将传递给 log,而 log 的返回值将转发给 store。通过组合函数调用来构建语句,我们最终可以从原子构建块构建复杂的算法,但是嵌套这些调用可能会变得难以处理;如果我们想组合 10 个函数,那会是什么样子?
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
幸运的是,我们可以使用一个优雅的通用实现:将函数数组简化为高阶函数。
使用 Array.prototype.reduce 累积数组
Array 原型的 reduce 方法接受一个数组实例并将其累积为单个值。如果我们希望对数字数组求和,可以使用以下方法:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
在此代码段中,numbers.reduce 接受两个参数:将在每次迭代时调用的回调函数,以及传递给该回调函数的 total 参数的初始值;回调函数返回的值将在下一次迭代中传递给 total。为了通过研究对 sum 的上述调用来进一步分解这一点:
虽然回调函数接受另外两个参数,它们分别表示当前索引和调用 Array.prototype.reduce 的数组实例,但前两个是最关键的,通常被称为:
使用 Array.prototype.reduce 组合函数
现在我们了解了如何将数组简化为单个值,我们可以使用这种方法将现有函数组合成新函数:
const getFullName = person => `${person.firstName} ${person.surname}`;
请注意,我们使用 rest 参数语法 (...) 将任意数量的参数强制转换为数组,从而使使用者无需为每个调用站点显式创建新的数组实例。compose 还返回另一个函数,这使得 compose 成为一个高阶函数,它接受一个初始值 (initialArg)。这至关重要,因为我们因此可以组合新的、可重用的函数,而无需在必要之前调用它们;这被称为惰性求值。
那么我们如何将其他函数组合到单个高阶函数中呢?
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
在此代码中:
调用顺序的重要性
能够使用可组合实用程序组合任意数量的函数使我们的代码更简洁、抽象性更好。但是,我们可以通过重新访问内联调用来突出一个重点:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
人们可能会发现用我们的 compose 函数复制这个是自然的:
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
在这种情况下,为什么 fNested(4) === fComposed(4) 解析为 false?您可能记得我强调了如何首先解释内部调用,因此 compose(g, h, i) 实际上等效于 x => i(h(g(x))),因此 fNested 返回 10 而 fComposed 返回 9。我们可以简单地反转 f 的嵌套或组合变体的调用顺序,但是鉴于 compose 旨在镜像嵌套调用的特异性,我们需要一种方法以从右到左的顺序减少函数;JavaScript 幸运地使用 Array.prototype.reduceRight 提供了这一点:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
使用此实现,fNested(4) 和 fComposed(4) 都解析为 10。但是,我们的 getPersonWithSideEffects 函数现在定义不正确;尽管我们可以反转内部函数的顺序,但在某些情况下,从左到右读取可以促进对程序步骤的心理解析。事实证明,我们之前的方法已经相当普遍,但通常被称为管道:
const getFullName = person => `${person.firstName} ${person.surname}`;
通过使用我们的 pipe 函数,我们将保持 getPersonWithSideEffects 所需的从左到右的顺序。由于前面概述的原因,管道已成为 RxJS 的主要组成部分;从这个顺序来看,思考由运算符操作的组合流中的数据流可能更直观。
函数组合作为继承的替代方案
我们已经在前面的示例中看到,如何将无限多个函数组合成更大、可重用、面向目标的单元。函数组合的另一个好处是使自己摆脱继承图的僵化性。假设我们希望根据类的层次结构重用日志记录和存储行为;可以这样表达:
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
除了冗长之外,这段代码的直接问题是我们滥用继承来实现重用;如果另一个类扩展 Loggable,它也固有地是 Storable 的子类,即使我们不需要此逻辑。一个潜在的更严重的问题在于命名冲突:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
如果我们要实例化 MyState 并调用其 store 方法,除非我们在 MyState.prototype.store 中添加对 super.store() 的调用,否则我们将不会调用 Storable 的 store 方法,但这会在 State 和 Storable 之间创建一个紧密、脆弱的耦合。这可以使用实体系统或策略模式来缓解,正如我在其他地方所介绍的那样,但是尽管继承表达系统更广泛分类法的优势,函数组合提供了一种扁平的、简洁的方法来共享不依赖于方法名称的代码。
总结
JavaScript 将函数作为值处理,以及生成它们的表达式,使其能够轻松组合更大、特定于上下文的作业。将此任务视为函数数组的累积消除了对命令式嵌套调用的需求,并且使用高阶函数会导致分离它们的定义和调用。此外,我们可以摆脱面向对象编程强加的严格层次结构约束。
关于 JavaScript 中函数组合的常见问题解答
函数组合是 JavaScript 和大型函数式编程中的一个基本概念。它允许开发人员通过组合更简单的函数来创建复杂的函数,从而提高代码的可重用性和模块化。这种方法使代码更容易理解、调试和测试。它还鼓励“不要重复自己”(DRY)的原则,减少代码库中的冗余。
高阶函数是 JavaScript 中函数组合的关键部分。高阶函数是可以将一个或多个函数作为参数、返回一个函数作为结果或同时执行这两项操作的函数。在函数组合中,我们经常使用高阶函数通过组合现有函数来创建新函数。
当然,让我们考虑一个简单的示例。假设我们有两个函数,double 和 increment。double 接受一个数字并将其乘以 2,而 increment 将 1 加到其输入中。我们可以组合这两个函数来创建一个新的函数,该函数将一个数字加倍,然后递增结果。
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
函数组合和函数链是组合 JavaScript 中函数的两种不同方法。函数组合涉及将一个函数的输出作为另一个函数的输入传递。另一方面,函数链涉及按顺序调用多个函数,其中每个函数都调用前一个函数的结果。虽然这两种技术都可以获得类似的结果,但函数组合更符合函数式编程的原则。
函数组合促进创建小型纯函数,这些函数只做一件事并且做得很好。与大型单片函数相比,这些函数更容易测试和调试。由于每个函数都是独立的,因此您可以单独测试它,而无需担心其余代码。这使得更容易找到并修复代码中的错误。
是的,函数组合可以与 JavaScript 中的异步函数一起使用。但是,这需要更多注意,因为您需要确保一个异步函数的输出正确地作为输入传递给下一个函数。这通常涉及使用 promise 或 async/await 语法。
虽然函数组合有很多好处,但如果使用不当,它也可能会引入复杂性。例如,深度嵌套的函数调用可能难以阅读和理解。此外,如果组合的函数不是纯函数(即它们具有副作用),则可能会导致意外结果。因此,务必谨慎使用函数组合并结合良好的编码实践。
柯里化是 JavaScript 中的一种技术,其中具有多个参数的函数被转换为一系列函数,每个函数只有一个参数。柯里化可以与函数组合一起使用以创建更灵活和可重用的函数。事实上,一些实用程序库(如 lodash 和 Ramda)同时提供了柯里化和组合函数。
是的,函数组合可以与 React 或 Vue 等 JavaScript 框架一起使用。事实上,在 React 中,组合组件以构建复杂的用户界面是一种常见模式。类似地,Vue 的 mixin 系统可以看作是一种函数组合的形式。
是的,有几个库提供了 JavaScript 中函数组合的实用程序。一些流行的库包括 lodash、Ramda 和 Redux(用于状态管理)。这些库提供了 compose 或 pipe 等函数,使组合函数变得更容易、更高效。
以上是JavaScript中的功能组成的详细内容。更多信息请关注PHP中文网其他相关文章!