JavaScript 函数式管道的简单解释

2025-05-24

JavaScript 函数式管道的简单解释

有时我会被问到为什么 RxJS 中不再使用“点链”,或者为什么 RxJS 转而使用pipe。原因有很多,但这确实需要从比 RxJS 更高的层次来看待。

管道函数的需求源于两个相互冲突的问题:希望拥有一套适用于简单类型(如 Array、Observable、Promise 等)的广泛可用开发 API,以及希望发布更小的应用程序。

尺寸问题

JavaScript 是一种非常独特的语言,但它有一个大多数其他编程语言都没有的问题:通常,JavaScript 是在用户想要使用 JavaScript 所驱动的应用时通过网络传输、解析和执行的。传输的 JavaScript 越多,下载和解析所需的时间就越长,从而降低应用的响应速度。这会对用户体验造成巨大的影响。

这意味着,努力保持 JavaScript 应用的精简至关重要。幸运的是,如今我们有很多优秀的工具可以做到这一点。我们有很多“构建时”打包器和优化器,它们可以执行诸如 tree-shaking 之类的操作,以便在构建时提前清除未使用的代码,从而尽可能减少 JavaScript 代码量。

不幸的是,如果不能静态地确保代码没有在某处被使用,那么摇树就不会删除代码。

提供广泛的 API

为了使类型尽可能实用,最好将一组精心设计的已知功能附加到该类型上。特别是这样,可以通过从左到右对该类型进行调用来实现“链式调用”。

JavaScript 为给定类型提供广泛 API 的“内置”方式是原型扩充。这意味着你可以向任何给定类型的prototype对象添加方法。因此,如果我们想向数组添加自定义odds过滤器,可以这样做:

Array.prototype.odds = function() {
  return this.filter(x => x % 2 === 1)
}

Array.prototype.double = function () {
  return this.map(x => x + x);
}

Array.prototype.log = function () {
  this.forEach(x => console.log(x));
  return this;
}
Enter fullscreen mode Exit fullscreen mode

原型增强存在问题

修改全局变量。你现在操作的是其他人都能接触到的东西。这意味着其他代码可能会依赖这个odds方法在 上执行Array,而不知道它实际上来自第三方。这也意味着另一段代码可能会通过odds它自己定义的来破坏它odds。有一些解决方案,比如使用Symbol,但这仍然不是理想的选择。

原型方法无法进行 tree-shaking。打包器目前不会尝试移除已添加到原型的未使用方法。具体原因请参见上文。打包器无法获知是否有第三方依赖该原型方法。

函数式编程 FTW!

一旦你意识到上下文this实际上只是将另一个参数传递给函数的一种奇特方式,你就会意识到你可以像这样重写上述方法:

function odds(array) {
  return array.filter(x => x % 2 === 0);
}

function double(array) {
  return array.map(x => x + x);
}

function log(array) {
  array.forEach(x => console.log(x));
  return array;
}
Enter fullscreen mode Exit fullscreen mode

现在的问题是,您必须从右到左读取数组中发生的事情,而不是从左到右:

// Yuck!
log(double(odds([1, 2, 3, 4, 5])))
Enter fullscreen mode Exit fullscreen mode

不过,优点是,如果我们不使用它double,那么捆绑器将能够进行树形摇动并double从发送给用户的最终结果中删除该功能,从而使您的应用程序更小、更快。

管道设计,方便从左到右阅读

为了获得更好的从左到右的可读性,我们可以使用pipe函数。这是一个常见的函数模式,可以用一个简单的函数来实现:

function pipe(...fns) {
  return (arg) => fns.reduce((prev, fn) => fn(prev), arg);
}
Enter fullscreen mode Exit fullscreen mode

它的作用是返回一个新的高阶函数,该函数接受一个参数。返回的函数会将参数传递给函数列表中的第一个函数fns,然后获取其结果,并将其传递给列表中的下一个函数,依此类推。

这意味着我们现在可以从左到右编写这些内容,这样更易​​读一些:

pipe(odds, double, log)([1, 2, 3, 4, 5])
Enter fullscreen mode Exit fullscreen mode

您还可以创建一个助手,允许您提供该参数作为第一个参数,以使其更具可读性(如果可重用性稍差),如下所示:

function pipeWith(arg, ...fns) {
  return pipe(...fns)(arg);
}

pipeWith([1, 2, 3, 4, 5], odds, double, log);
Enter fullscreen mode Exit fullscreen mode

对于pipeWith,现在它将获取第一个参数,并将其传递给参数列表中紧接着它的函数,然后它将获取其结果并将其传递给参数列表中的下一个函数,依此类推。

带有参数的“可管道化”函数

要创建一个可以通过管道传输但带有参数的函数,只需使用高阶函数即可。例如,如果我们想创建一个multiplyBy函数来代替double

pipeWith([1, 2, 3, 4, 5], odds, multiplyBy(2), log);

function multiplyBy(x) {
  return (array) => array.map(n => n * x);
}
Enter fullscreen mode Exit fullscreen mode

作品

因为它们只是函数,所以您可以通过pipe创建其他可重用和可管道化的函数来简化代码并使其更具可读性!

const tripleTheOdds = pipe(odds, multiplyBy(3));


pipeWith([1, 2, 3, 4, 5], tripleTheOdds, log)
Enter fullscreen mode Exit fullscreen mode

更大的 JS 生态系统和管道运算符

这与RxJS 操作符通过 Observable方法使用的模式大致相同pipe。这样做是为了解决上面提到的所有原型问题。但这显然适用于任何类型。

虽然prototype增强可能是 JavaScript 中向类型添加方法的“幸运”方式,但在我看来,它有点反模式。JavaScript 需要更多地拥抱这种模式,理想情况下,我们可以在 JavaScript 中实现一个简化版本的管道操作符提案

使用管道运算符,上述代码可能看起来像这样,但功能相同,并且不需要声明pipe帮助程序。

pipeWith([1, 2, 3, 4, 5], odds, double, log);

// becomes

[1, 2, 3, 4, 5] |> odds |> double |> log
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/benlesh/a-simple-explanation-of-function-pipe-in​​-javascript-2hbj
PREV
开始构建 Web Components!第一部分:标准
NEXT
如何在 Next.js 中思考