Pipeline Operator S2
中文标题:管道运算符(
\|>)
- 阶段: Stage 2
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案引入了一个 Hack 风格的管道运算符 \|>,并伴有一个主题引用标记 %,允许在管道右侧放置任何表达式。它旨在将深层嵌套的表达式线性化,并提供一种比方法链式调用和临时变量更易读且适用性更广的替代方案。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
JavaScript 的管道运算符(|>)
- 阶段:2
- 倡导者:J. S. Choi、James DiGioia、Ron Buckton、Tab Atkins-Bittner,[列表不完整]
- 前倡导者:Daniel Ehrenberg
- [规范
- [贡献指南
- [提案历史
- Babel 插件:在 v7.15 中已实现。请参阅 Babel 文档。
(本文档使用 % 作为主题引用的占位符标记。这几乎肯定不会是最终选择;
有关详细信息,请参阅标记争论讨论。)
为什么需要管道运算符
在《State of JS 2020》调查中,对 “你认为 JavaScript 目前缺少什么?” 的第四大热门回答是管道运算符。为什么?
当我们在 JavaScript 中对值执行连续操作(例如,函数调用)时, 目前有两种基本风格:
- 将值作为参数传递给操作 (如果有多个操作,则嵌套这些操作),
- 或将函数作为值的方法调用 (如果有多个方法,则链接更多方法调用)。
也就是说,three(two(one(value))) 与 value.one().two().three()。
然而,这两种风格在可读性、流畅性和适用性方面差异很大。
深层嵌套难以阅读
第一种风格,嵌套,通常适用——
它适用于任何操作序列:
函数调用、算术、数组/对象字面量、await 和 yield 等。
然而,当嵌套变深时,难以阅读: 执行流程从右到左移动, 而不是正常代码的从左到右阅读。 如果在某些层级有多个参数, 阅读甚至会在来回跳动: 我们的眼睛必须向左跳以找到函数名, 然后必须向右跳以找到其他参数。 此外,事后编辑代码可能充满风险: 我们必须在许多嵌套的括号中找到插入新参数的正确位置。
现实世界示例
考虑这个来自 React 的现实世界代码。
这段现实世界代码由深层嵌套的表达式组成。 为了阅读其数据流,人眼必须首先:
-
找到初始数据(最内层的表达式,
envars)。 -
然后从内到外反复来回扫描, 对于每个数据转换, 要么是左侧容易忽略的前缀运算符, 要么是右侧的后缀运算符:
Object.keys()(左侧),.map()(右侧),.join()(右侧),- 模板字面量(两侧),
chalk.dim()(左侧),然后console.log()(左侧)。
由于深度嵌套多个表达式的结果 (其中一些使用前缀运算符, 一些使用后缀运算符, 还有一些使用环绕运算符), 我们必须检查左右两侧 才能找到每个表达式的头部。
方法链式调用有限制
第二种风格,方法链式调用,仅当值具有其类指定的方法函数时才可用。 这限制了其适用性。 但当它适用时,由于其后缀结构, 通常更实用,更容易读写。 代码执行从左到右流动。 深层嵌套的表达式被解开。 函数调用的所有参数都与函数名分组。 并且在之后插入或删除更多方法调用的代码编辑是微不足道的, 因为我们只需将光标放在一个位置, 然后开始输入或删除一个连续的字符序列。
事实上,方法链式调用的好处如此吸引人, 以至于一些流行的库扭曲其代码结构, 专门允许更多的方法链式调用。 最突出的例子是 jQuery,它仍然是世界上最流行的 JS 库。 jQuery 的核心设计是包含数十个方法的单一超级对象, 所有这些方法都返回相同的对象类型,以便我们可以继续链式调用。 这种编程风格甚至有一个名字:流畅接口。
不幸的是,尽管它很流畅,
仅凭方法链式调用无法适应 JavaScript 的其他语法:
函数调用、算术、数组/对象字面量、await 和 yield 等。
这样,方法链式调用在适用性上仍然是有限的。
管道运算符结合了两者的优点
管道运算符试图将方法链式调用的便利性和易用性 与表达式嵌套的广泛适用性结合起来。
所有管道运算符的一般结构是
value |> e1 |> e2 |> e3,
其中 e1、e2、e3
都是将连续值作为其参数的表达式。
|> 运算符随后进行某种程度的“魔法”,将 value
从左侧“管道”到右侧。
现实世界示例,续
继续这个来自 React 的深层嵌套现实世界代码:
…我们可以使用管道运算符和代表前一个操作值的占位符标记(%)将其解开如下:
现在,人类读者可以快速找到****初始数据
(曾经是最内层表达式,envars),
然后从左到右****线性阅读
对数据的每个转换。
临时变量通常很繁琐
有人可能会说,使用临时变量 应该是解开深层嵌套代码的唯一方法。 显式命名每一步的变量 会产生类似于方法链式调用的效果, 对阅读和编写代码有类似的好处。
但我们在现实世界中经常遇到彼此的代码中出现深层嵌套表达式, 而不是一系列临时变量,是有原因的。 并且 jQuery、Mocha 等的基于方法链的[流畅接口仍然流行,也是有原因的。
编写带有长序列临时、一次性变量的代码通常过于繁琐和冗长。 可以说,人类阅读起来甚至也会感到繁琐和视觉上的噪音。
如果 命名是编程中最困难的任务之一, 那么当程序员认为收益相对较小时, 他们将不可避免地避免命名变量。
重用临时变量容易出现意外修改
有人可能会说,使用一个短名称的可变变量 可以减少临时变量的冗长,达到与管道运算符相似的结果。
但这样的代码在现实世界的代码中并不常见。 一个原因是可变变量可能意外更改, 导致难以发现的静默错误。 例如,变量可能被意外地在闭包中引用。 或者它可能在表达式内被错误地重新赋值。
示例代码
使用管道运算符不会出现这个问题。 主题标记不能被重新赋值,并且 每一步之外的代码不能更改其绑定。
因此,带有可变变量的代码也更难阅读。 要确定变量在任何给定点代表什么, 您必须搜索整个前置作用域以查找其被重新赋值的位置。
另一方面,管道的主体引用具有有限的词法作用域, 并且其绑定在其作用域内是不可变的。 它不能被意外地重新赋值,并且可以安全地在闭包中使用。
尽管主题值也随每个管道步骤而改变, 但我们只需扫描管道的前一步即可理解它, 从而使代码更易于阅读。
临时变量必须在语句中声明
管道运算符相对于赋值语句序列 (无论是使用可变还是不可变的临时变量)的另一个好处 是它们是表达式。
管道表达式是可直接返回、 赋值给变量或在诸如 JSX 表达式等上下文中使用的表达式。
另一方面,使用临时变量则需要语句序列。
示例
为什么选择 Hack 管道运算符
对于管道运算符,有两个竞争提案:Hack 管道和 F# 管道。 (在此之前,有一个针对前两个提案“智能混合”的第三个提案, 但它已被撤回, 因为其语法严格是其中一个提案的超集。)
这两个管道提案在使用 |> 拼写代码时,“魔法”方面略有不同。
两个提案都重用了现有的语言概念: Hack 管道基于表达式的概念, 而 F# 管道基于一元函数的概念。
管道表达式和管道一元函数 相应地具有微小且几乎对称的权衡。
本提案:Hack 管道
在 Hack 语言的管道语法中,
管道的右侧是一个包含特殊占位符的表达式,
当占位符绑定到左侧表达式的结果时,对该表达式求值。
也就是说,我们编写 value |> one(%) |> two(%) |> three(%)
来将 value 通过这三个函数进行管道。
优点: 右侧可以是任何表达式, 占位符可以出现在任何普通变量标识符可以出现的地方, 因此我们可以无需任何特殊规则地将值管道到任何我们想要的代码:
value |> foo(%)用于一元函数调用,value |> foo(1, %)用于 n 元函数调用,value |> %.foo()用于方法调用,value |> % + 1用于算术,value |> [%, 0]用于数组字面量,value |> {foo: %}用于对象字面量,value |> `${%}`用于模板字面量,value |> new Foo(%)用于构造对象,value |> await %用于等待 Promise,value |> (yield %)用于产出生成器值,value |> import(%)用于调用类似函数的关键字,- 等等。
缺点: 通过一元函数进行管道
使用 Hack 管道比使用 F# 管道稍微冗长。
这包括由 函数柯里化 库(如 Ramda)创建的一元函数,
以及 对其参数执行复杂解构的一元箭头函数:
使用 Hack 管道会因显式的函数调用后缀 (%) 而略微冗长。
(当 do 表达式取得进展时, 对主题值进行复杂解构将更容易, 因为您将能够在管道体内进行变量赋值/解构。)
备选提案:F# 管道
在 F# 语言的管道语法中,
管道的右侧是一个表达式,
该表达式必须求值为一元函数,
然后用左侧的值作为其唯一参数****隐式调用。
也就是说,我们编写 value |> one |> two |> three 来将 value
通过这三个函数进行管道。
left |> right 变为 right(left)。
这被称为隐式编程或无点风格。
优点: 右侧必须解析为一元函数的限制 使我们能够编写非常简洁的管道 当我们想要执行的操作是一元函数调用时:
value |> foo用于一元函数调用。
这包括由 函数柯里化 库(如 Ramda)创建的一元函数,
以及 对其参数执行复杂解构的一元箭头函数:
使用 F# 管道会因隐式的函数调用(没有 (%))而略微不冗长。
缺点: 这种限制意味着任何由其他语法执行的操作 都必须通过将操作包装在一元箭头函数中来略微冗长:
value |> x=> x.foo()用于方法调用,value |> x=> x + 1用于算术,value |> x=> [x, 0]用于数组字面量,value |> x=> ({foo: x})用于对象字面量,value |> x=> `${x}`用于模板字面量,value |> x=> new Foo(x)用于构造对象,value |> x=> import(x)用于调用类似函数的关键字,- 等等。
即使调用命名函数,当我们需要传递多个参数时也需要包装:
value |> x=> foo(1, x)用于 n 元函数调用。
缺点: await 和 yield 操作限定在其包含函数的范围内,
因此无法仅由一元函数处理。
如果我们想将它们集成到管道表达式中,
await 和 yield 必须作为特殊语法情况处理:
value |> await用于等待 Promise,以及value |> yield用于产出生成器值。
Hack 管道更有利于常见的表达式
Hack 管道和 F# 管道都分别对不同表达式施加了微小的语法税:
Hack 管道仅对一元函数调用略微征税,并且
F# 管道对所有除一元函数调用外的表达式略微征税。
在两个提案中,每个被征税表达式的语法税都很小
(两者的 (%) 和 x=> 都只有三个字符)。
然而,税额会乘以其各自被征税表达的普遍程度。
因此,可能有必要
对不常见的表达式征税,
并优化以利于更常见的表达式。
一元函数调用通常不如除一元函数外的所有表达式常见。 特别是,方法调用和 n 元函数调用 总是会流行; 在一般频率上, 一元函数调用的频率等于或超过 这两种情况单独的总和—— 更不用说其他普遍的语法 ,例如数组字面量、对象字面量 和算术运算。 本解释器包含几个关于这种普遍性差异的真实世界示例。
此外,其他几个提议的新语法, 例如 扩展调用、 do 表达式、 和 记录/元组字面量, 也可能会在将来变得普遍。 同样,如果 TC39 标准化了**运算符重载, 算术运算也会变得更加常见**。 解开这些未来语法表达式将 使用 Hack 管道比使用 F# 管道更流畅。
Hack 管道可能更易于使用
Hack 管道对一元函数调用的语法税
(即调用右侧一元函数的 (%))
不是特殊情况:
它只是显式编写普通代码,
以我们通常在没有管道的情况下的方式编写。
另一方面,F# 管道要求我们区分 “解析为一元函数的代码”与 “任何其他表达式” —— 并记住为后者添加箭头函数包装器。
例如,使用 Hack 管道,value |> someFunction + 1
是无效语法,并且会提前失败。
无需识别 someFunction + 1
不会求值为一元函数。
但使用 F# 管道,value |> someFunction + 1 仍然是有效语法——
它只会在运行时****延迟失败,
因为 someFunction + 1 不可调用。
TC39 已多次否决 F# 管道
管道倡导小组已向 TC39 两次提出 F# 管道进入阶段 2。 两次都未能成功推进到阶段 2。 F# 管道(以及 部分函数应用 (PFA)) 遭到了多位其他 TC39 代表的强烈反对, 原因多种多样。包括:
- 内存性能问题(例如,特别是来自浏览器引擎实现者),
- 关于
await的语法问题。 - 对鼓励生态系统分裂/分叉等的担忧。
这种反对来自管道倡导小组之外。 有关更多信息,请参阅 HISTORY.md。
管道倡导小组认为,任何管道运算符总比没有好, 以便轻松线性化深度嵌套的表达式 而无需诉诸命名变量。 倡导小组的许多成员认为 Hack 管道比 F# 管道稍好, 并且倡导小组的一些成员认为 F# 管道比 Hack 管道稍好。 但倡导小组中的每个人都同意,F# 管道遇到了太多的阻力, 在可预见的将来无法通过 TC39。
需要强调的是,尝试从 Hack 管道切换回 F# 管道 很可能导致 TC39 永远不会同意任何管道。 PFA 语法在 TC39 中同样面临艰巨的战斗(参见 HISTORY.md)。 管道倡导小组的许多成员认为这很遗憾, 并且他们愿意在稍后再次为 F#-管道混合 和 PFA 语法 而战。 但管道倡导小组之外 有不少代表(包括浏览器引擎实现者) 普遍反对鼓励 隐式编程(和 PFA 语法), 无论 Hack 管道如何。
描述
(提供了一份正式的草案规范。)
主题引用 % 是一个零元运算符。
它充当主题值的占位符,
并且是词法作用域和不可变的。
% 不是最终选择
(主题引用的确切标记 不是最终。
% 可以替换为 ^,或许多其他标记。
我们计划在推进到阶段 3 之前
争论实际使用什么标记。
然而,% 似乎是语法上问题最少的,
并且它类似于 printf 格式字符串 和
Clojure 的 #(%) 函数字面量的占位符。)
管道运算符 |> 是一个中缀运算符,
它形成一个管道表达式(也称为管道)。
它计算其左侧(管道头或管道输入),
将结果值(主题值)不可变地绑定到主题引用,
然后在该绑定下计算其右侧(管道体)。
右侧的结果值
成为整个管道表达式的最终值(管道输出)。
管道运算符的优先级与以下相同:
- 函数箭头
=>; - 赋值运算符
=、+=等; - 生成器运算符
yield和yield *;
它仅比逗号运算符 , 更紧。
它比所有其他运算符更松。
例如,v => v |> % == null |> foo(%, 0)
将分组为 v => (v |> (% == null) |> foo(%, 0)),
这反过来等价于 v => foo(v == null, 0)。
管道体必须至少使用其主题值一次。
例如,value |> foo + 1 是无效语法,
因为其主体不包含主题引用。
这种设计是因为从管道表达式的主体中省略主题引用
几乎可以肯定是意外的程序员错误。
同样,主题引用必须包含在管道体中。 在管道体外使用主题引用 也是无效语法。
为了防止混淆分组,
使用类似优先级的其他运算符
(即箭头 =>、三元条件运算符 ? :、
赋值运算符和 yield 运算符)
作为管道头或主体是无效语法。
当将 |> 与这些运算符一起使用时,我们必须使用括号
来明确指示正确的分组。
例如,a |> b ? % : c |> %.d 是无效语法;
应该将其更正为 a |> (b ? % : c) |> %.d
或 a |> (b ? % : c |> %.d)。
最后,动态编译代码内的主题绑定
(例如,使用 eval 或 new Function)
不能在该代码之外使用。
例如,v |> eval('% + 1') 将在运行时评估 eval 表达式时抛出一个语法错误。
没有其他特殊规则。
这些规则的一个自然结果是,
如果我们需要在管道表达式链中间插入一个副作用,
而不修改正在被管道传输的数据,
那么我们可以使用逗号表达式,
例如 value |> (sideEffect(), %)。
像往常一样,逗号表达式将计算为其右侧 %,
实质上传递主题值而不修改它。
这对于快速调试特别有用:value |> (console.log(%), %)。
现实世界的例子
对原始示例的唯一更改是减少缩进和删除注释。
来自 jquery/build/tasks/sourceMap.js:
来自 node/deps/npm/lib/unpublish.js:
来自 underscore.js:
来自 ramda.js。
来自 ramda.js。
来自 react/scripts/jest/jest-cli.js。
来自 ramda.js。
与其他提案的关系
Function 帮助函数
Hack 管道可以且将能够与 Function 帮助函数提案共存,
包括其 pipe 和 flow 函数。
这些简单(且经常下载)的便捷函数
无需额外语法即可操作一元函数。
TC39 已经两次否决了 F# 管道运算符。
鉴于这一现实,TC39 通过 pipe 和 flow 帮助函数的可能性
比通过类似语法运算符的可能性要大得多。
标准化的 pipe 和 flow 便捷函数
也可能消除对 F# 管道中缀运算符的一些需求。
(它们不会妨碍以后标准化等效的运算符。
例如,即使 Math.pow 存在,TC39 也标准化了二元 **。)
部分函数应用语法
Hack 管道可以与部分函数应用(PFA)的语法共存。 它们可以共存有两种方法。
第一种方法是使用急切求值的 PFA 语法,
这已经在 proposal-partial-application 中提出。
这种急切 PFA 语法将添加一个 …~(…) 运算符。
该运算符的右侧将是一个参数列表,
每个参数要么是普通表达式,要么是 ? 占位符。
每个连续的 ? 占位符代表另一个参数。
普通表达式将在创建函数之前求值。
例如,f~(g(), ?, h(), ?) 将求值 f,然后求值 g(),然后求值 h(),
然后它将创建一个带有两个参数的 f 的部分应用版本。
? 占位符后的可选数字
将覆盖参数的位置。
例如,f~(?1, ?0) 将有两个参数,但在调用 f 时会交换它们。
第二种方法是使用惰性求值的语法。
这可以通过 Hack 管道的扩展来实现,
该扩展的语法进一步受到
Clojure 的 #(%1 %2) 函数字面量的启发。
它将通过组合 Hack 管道 |>
和箭头函数 =>
成为一个 管道函数 运算符 +>,
该运算符将使用与 |> 相同的一般规则。
+> 将是一个前缀运算符,它创建一个新函数,
该函数又将其参数绑定到主题引用。
非一元函数将通过包含带有数字的主题引用(%0、%1、%2 等)或 ... 来创建。
%0(相当于普通的 %)将绑定到第零个参数,
%1 将绑定到下一个参数,依此类推。
%... 将绑定到剩余参数的数组。
并且就像使用 |> 一样,+> 将要求其主体
包含至少一个主题引用
才能语法有效。
与急切求值的 PFA 语法相反, 主题函数将惰性地求值其参数, 就像箭头函数一样。
例如,+> f(g(), %0, h(), %1) 将求值 f,
然后它将创建一个关闭了 g 和 h 的箭头函数。
创建的函数不会求值 g() 或 h(),
直到创建的函数每次被调用时。
无论采取哪种方法,Hack 管道都可以与 PFA 共存。
最终发送 / 管道化
尽管名称中共享了“pipe”, 管道运算符和 最终发送提案 的远程对象管道 是正交且独立的。 它们可以共存,甚至协同工作。
可能的未来扩展
用于 if、catch 和 for–of 的 Hack 管道语法
许多 if、catch 和 for 语句可以变得更简洁
如果它们获得了绑定主题引用的“管道语法”。
if () |> 将把其条件值绑定到 %,
catch |> 将把它捕获的错误绑定到 %,
for (of) |> 将把其迭代器的每个值连续绑定到 %。
可选 Hack 管道
一个短路的可选管道运算符 |?> 也可能有用,
就像 ?. 对于可选方法调用很有用一样。
例如,value |> (% == null ? % : await foo(%) |> (% == null ? % : % + 1))
将等价于 value |?> await foo(%) |?> % + 1。
隐式一元函数应用语法
用于隐式一元函数应用的语法——即 F# 管道运算符—— 已经被 TC39 两次否决。 然而,它们仍然可以最终以两种方式添加到语言中。
首先,它可以作为便捷函数 Function.pipe 添加。
这就是 function-helpers 提案所提议的。
Function.pipe 可能会消除对 F# 管道运算符的大部分需求,
同时仍然不会关闭 F# 管道运算符的可能性。
其次,它可以作为另一个管道运算符 |>> 添加——
类似于 Clojure 有多个管道宏
->、->> 和 as->。
例如,value |> % + 1 |>> f |> g(%, 0)
将意味着 value |> % + 1 |> f(%) |> g(%, 0)。
有一个 关于两个管道运算符的这种混合的非正式提案, 它被搁置,取而代之的是单运算符提案。 这种混合可能会在 Hack 管道之后作为提案回归。