For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/1/proposal-partial-application.md.
  • 简体中文
  • Partial application S1

    中文标题:部分应用

    提案概览
    提案速览

    该提案引入了一种新的 ~() 调用约定,允许对调用或 new 表达式进行参数的部分应用,并使用 ? 作为未绑定参数的占位符,支持序数占位符(?0)和 . 剩余占位符。它旨在克服 Function.prototype.bind 和箭头函数的局限性,例如只能固定前导参数或惰性求值。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    ECMAScript 的部分应用语法

    本提案引入了一种新的调用约定(使用 ~())的语法,允许您通过组合_应用参数_(实际值)和_占位参数_(未绑定参数,成为结果_部分应用函数_中的参数)来将参数列表部分应用到调用或 new 表达式。

    状态

    阶段: 1
    ** champions:** Ron Buckton (@rbuckton)

    有关更多信息,请参阅 TC39 提案流程

    作者

    • Ron Buckton (@rbuckton)

    提案

    部分函数应用允许您将函数调用中的一部分参数固定,并返回一个新函数。目前在 ECMAScript 中,部分应用可以通过使用 Function.prototype.bind 或箭头函数来支持:

    function add(x, y) { return x + y; }
    
    // Function.prototype.bind
    const addOne = add.bind(null, 1);
    addOne(2); // 3
    
    // 箭头函数
    const addTen = x => add(x, 10);
    addTen(2); // 12

    但是,这些方法有几个局限性:

    • Function.prototype.bind 只能固定函数的前导参数。
    • Function.prototype.bind 要求您显式指定 this 接收者。
    • 箭头函数会惰性重新评估其函数体,这可能会引入意外的副作用。

    为了解决这些问题,我们提议引入以下新的语言特性:

    • 一种新的调用约定 ~(),用于表示调用或 new 表达式的_部分应用_,其结果是一个_部分应用函数_。
    • 使用 ? 标记作为_占位参数_,用于表示_部分应用_中的任何未固定参数。
    • 使用 ? 标记后跟一个十进制整数(即 ?0)作为_序数占位参数_,用于绑定到结果_部分应用函数_中特定序号参数的未固定参数。
    • 使用 ... 标记作为_剩余占位参数_,用于表示任何多余的参数。
    const add = (x, y) => x + y;
    const identity = x => x;
    
    // 从左侧应用:
    const addOne = add~(1, ?);
    addOne(2); // 3
    
    // 从右侧应用:
    const addTen = add~(?, 10);
    addTen(2); // 12
    
    // 接受固定的参数列表:
    const numbers = ["1", "2", "3"].map(parseInt~(?, 10)); // [1, 2, 3]
    
    // 指定序数占位参数:
    const indices = [1, 2, 3].map(identity~(?1)); // [0, 1, 2]
    
    // 绑定 `console` 作为接收者并仅接受一个参数:
    [1, 2, 3].forEach(console.log~(?));
    // 打印:
    // 1
    // 2
    // 3
    
    // 模拟 Function.prototype.bind 的 n 元参数:
    const logger = console.log~("[service]", ...);
    logger("foo", "bar"); // 打印: [service] foo bar

    语法

    ~() 部分应用调用约定

    部分应用的调用使用与普通调用不同的调用约定。您不是使用 () 来调用或构造一个值,而是使用 ~() 发起部分调用。没有占位参数的部分应用调用基本上会将提供的任何参数固定到一个新函数中。如果被调用的表达式产生一个_引用_,则该_引用_的 this 绑定会被保留。默认情况下,提供给结果函数的额外参数会被忽略(有关更多信息,请参阅本文档后面的固定元数可变元数)。

    const sayNothing = console.log~();
    const sayHi = console.log~("Hello!");
    
    sayNothing();       // 打印:
    sayNothing("Shhh"); // 打印:
    
    sayHi();            // 打印: Hello!
    
    const bob = {
      name: "Bob",
      introduce() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    };
    
    const introduceBob = bob.introduce~();
    introduceBob();     // 打印: Hello, my name is Bob.

    这不是 ECMAScript 中第一个新的调用约定,它还有带标签的模板(即 tag`text${expr}`)和空值函数求值(即 f?.())。

    ? 占位参数

    ? 占位参数 可以在调用或 new 表达式的参数列表的顶层出现一次或多次(例如 f~(?)o.f~(?))。? 不是 一个表达式,而是一个表示特殊行为的语法元素(就像 `...` AssignmentExpression 表示展开,但其本身不是表达式)。

    // 有效
    f~(x, ?)          // 从左侧部分应用
    f~(?, x)          // 从右侧部分应用
    f~(?, x, ?)       // 任意参数的部分应用
    o.f~(x, ?)        // 从左侧部分应用
    o.f~(?, x)        // 从右侧部分应用
    o.f~(?, x, ?)     // 任意参数的部分应用
    super.f~(?)       // 允许对 |SuperProperty| 进行部分应用调用
    new C~(?)         // 构造函数的部分应用
    
    // 无效
    f~(x + ?)         // `?` 不在调用的顶层参数中
    x + ?             // `?` 不在调用的顶层参数中
    ?.f~()            // `?` 不在调用的顶层参数中
    super~(?)         // `?` 不支持在 |SuperCall| 中
    import~(?)        // `?` 不支持在 |ImportCall| 中

    ?0?1?2 等)序数占位参数

    ? 标记后可以跟一个大于等于 0 的十进制整数值(即 ?0),表示一个固定的序号位置,表示一个_序数占位参数_。序数占位参数对于将现有函数适配为需要不同参数顺序的回调函数特别有用:

    const printAB = (a, b) => console.log(`${a}, ${b}`);
    const acceptBA = (cb) => cb("b", "a");
    acceptBA(printAB~(?1, ?0));                // 打印: a, b

    此外,序数占位参数可以在部分应用内多次重复,允许重复引用同一个参数值:

    const add = (x, y) => x + y;
    const dup = add(?0, ?0);
    console.log(dup(3));                       // 打印: 6

    非序数占位参数隐式地从左到右依次排序。这意味着像 f~(?, ?) 这样的表达式本质上等同于 f~(?0, ?1)。如果部分应用混合了序数占位参数和非序数占位参数,序数占位参数不会影响分配给非序数占位参数的隐式顺序:

    const printABC = (a = "arg0", b = "arg1", c = "arg2") => console.log(`${a}, ${b}, ${c}`);
    printABC(1, 2, 3);                         // 打印: 1, 2, 3
    printABC();                                // 打印: arg0, arg1, arg2
    
    const printCAA = printABC~(?2, ?, ?0);     // 等同于: printABC~(?2, ?0, ?0)
    printCAA(1, 2, 3);                         // 打印: 3, 1, 1
    printCAA(1, 2);                            // 打印: arg0, 1, 1
    
    const printCxx = printABC~(?2);
    printCxx(1, 2, 3);                         // 打印: 3, arg1, arg2

    通过使序数占位参数独立于非序数占位参数的顺序,我们可以避免由于在现有部分应用中插入新的序数占位符而导致的重构风险。

    在第一个参数位置插入序数占位参数:

    -  const g = f~(?, ?, ?);                   // 等同于: f~(?0, ?1, ?2)
    +  const g = f~(?2, ?, ?, ?);               // 等同于: f~(?2, ?0, ?1, ?2)

    在其他占位参数之间插入序数占位参数:

    -  const g = f~(?, ?, ?);                   // 等同于: f~(?0, ?1, ?2)
    +  const g = f~(?, ?, ?0, ?);               // 等同于: f~(?0, ?1, ?0, ?2)

    固定元数

    默认情况下,部分应用使用固定参数列表:普通参数会被求值并绑定到它们各自的参数位置,占位参数(?)和序数占位参数(?0 等)会被绑定到结果部分应用函数中的特定参数位置。因此,传递给部分应用函数的额外参数没有特定的位置可以插入。虽然此行为与 f.bind() 不同,但固定参数列表使我们能够避免无意中接受多余的参数:

    // (a)
    [1, 2, 3].forEach(console.log.bind(console, "element:"));
    // 打印:
    // element: 1 0 1,2,3
    // element: 2 1 1,2,3
    // element: 3 2 1,2,3
    
    // (b)
    [1, 2, 3].forEach(x => console.log("element:", x));
    // 打印:
    // element: 1
    // element: 2
    // element: 3
    
    // (c)
    [1, 2, 3].forEach(console.log~("element:", ?));
    // 打印:
    // element: 1
    // element: 2
    // element: 3

    在上面的例子中,(a) 打印了无关的信息,因为 forEach 不仅将每个元素的值作为参数传递,还将元素的索引和包含该元素的数组作为参数传递。

    在 (b) 的情况下,箭头函数具有固定的元数。无论向回调传递多少额外参数,只有 x 参数会被转发到调用中。

    部分应用的意图是模拟像 console.log("element:", 1) 这样的正常调用,其中“已应用”部分会立即求值,只有占位参数是“未应用的”。这意味着多余参数在求值过程中没有去处。因此,(c) 的行为类似于 (b),因为部分函数应用只接受一个参数并将其传递给 console.log

    可变元数:使用 ... 传递剩余参数

    然而,有时您可能需要 Function.prototype.bind 提供的可变元数。为了支持这一点,部分应用包含一个 ... 剩余占位参数,其特定含义是:获取提供给部分函数的_剩余_参数并将它们_展开_到这个位置:

    const writeLog = (header, ...args) => console.log(header, ...args);
    const writeAppLog = writeLog~("[app]", ...);
    writeAppLog("Hello", "World!");
    // 打印:
    // [app] Hello World!
    
    const writeAppLogWithBreak = writeAppLog~(..., "\n---");
    writeAppLogWithBreak("End of section");
    // 打印:
    // [app] End of section
    // ---

    一个部分应用的参数列表中只能有一个 ... 剩余占位参数,不过您可以像在普通调用中一样,使用 ...expr 展开其他值:

    const arr = [1, 2, 3];
    
    // 以下是 SyntaxError,因为 `...` 占位符只能出现一次:
    // const g = console.log~(?, ..., ...);
    
    // 然而,普通的展开是完全有效的。下面,`...arr` 会立即求值并展开到已应用参数列表中:
    const g = console.log~(?, ...arr, ...);
    g("a", "b", "c");                           // 打印: a, 1, 2, 3, b, c

    语义

    使用 ~() 调用约定的调用或 new 表达式的结果是一个_部分应用函数_。这个结果是一个新函数,其参数对应于参数列表中的每个_占位参数_(即 ??0 等)。如果部分应用包含一个 ... 剩余占位参数,则会在结果_部分应用函数_中添加一个剩余参数作为最后一个参数。参数列表中的任何非占位参数都会被固定在其位置上。以下语法转换说明了这一点:

    const g = f~(?, 1, ?);

    其行为大致等同于:

    const g = (() => {
      // 已应用的值
      const _callee = f;
      const _applied0 = 1;
    
      // 部分应用函数
      return function (_0, _1) { return _callee(_0, _applied0, _1); };
    })();

    除了固定_被调用者_和任何_已应用参数_之外,我们还在结果_部分应用函数_中固定了 this 接收者。因此,o.f~(?) 在调用 o.f 时会保持 o 作为 this 接收者。这可以通过以下语法转换来说明:

    const g = o.f~(?, 1);

    其行为大致等同于:

    const g = (() => {
      // 已应用的值
      const _receiver_ = o;
      const _callee = _receiver_.f;
      const _applied0 = 1;
    
      // 部分应用函数
      return function (_0) { return _callee.call(_receiver_, _0, _applied0); };
    })();

    以下是其他语义规则的列表:

    • 给定 f~(),表达式 f 会立即求值,返回一个_部分应用函数_,它总是以无参数的方式调用 f 的值。
    • 给定 f~(?),表达式 f 会立即求值,返回一个具有单个参数的_部分应用函数_,它总是用该参数作为唯一参数调用 f 的值。
    • 给定 f~(?, x),非占位参数 x 会立即求值并固定在其位置。
    • 给定 f~(?),提供给部分应用函数结果的额外参数会被忽略。
    • 给定 f~(?, ?),部分应用函数的结果将为参数列表中的每个占位符标记在其对应位置生成一个参数。
    • 给定 f~(this, ?),参数列表中的 this 是词法上的 this
    • 给定 f~(?),函数 fthis 接收者在部分应用函数结果中被固定为 undefined
    • 给定 f~(?),部分应用函数结果的 length 等于参数列表中 ? 占位符标记的数量。
    • 给定 f~(?),部分应用函数结果的 namef.name
    • 给定 o.f~(?),对 oo.f 的引用会立即求值。
    • 给定 o.f~(?),函数 o.fthis 接收者在部分应用函数结果中被固定为 o
    • 给定 new C~(),结果是一个返回 C 新实例的函数。
      • 注意:这在今天使用 .bind() 并不容易实现(如果有的话)。
    • 给定 const g = new C~()g.name"bound C"
    • 给定 const g = new C~()g.length0(基于占位参数计算的长度)。
    • 给定 const g = new C~()Object.getPrototypeOf(g)C
    • 给定 new (f~()),对 f 的部分应用返回一个新函数,该函数可以通过 new 构造,类似于 new (f.bind(null))
    • 给定 f?.~()(部分应用的可选调用),如果 fnullundefined,则结果为 undefined。否则,结果是 f~() 的部分应用。
    • 给定 o?.f~()(可选链中的部分应用调用),如果 onullundefined,则结果为 undefined。否则,结果是 o.f~() 的部分应用。

    解析

    虽然本提案利用了条件表达式中现有的 ? 标记,但它不会引入解析歧义,因为 ? 占位符标记只能在参数列表中使用,并且不能有表达式在其前面(例如 f~(a? 肯定是条件表达式,而 f~(? 肯定是占位符)。

    语法

    MemberExpression[Yield, Await] :
      ...
      `new` MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await, ~Partial]
    
    CallExpression[Yield, Await] :
      CallExpression[?Yield, ?Await] Arguments[?Yield, ?Await, +Partial]
    
    CoverCallExpressionAndAsyncArrowHead[Yield, Await]:
      MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await, +Partial]
    
    CallMemberExpression[Yield, Await] :
      MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await, +Partial]
    
    SuperCall[Yield, Await] :
      `super` Arguments[?Yield, ?Await, ~Partial]
    
    OptionalChain[Yield, Await] :
      `?.` Arguments[?Yield, ?Await, +Partial]
      ...
      OptionalChain[?Yield, ?Await] Arguments[?Yield, ?Await, +Partial]
      ...
    
    Arguments[Yield, Await, Partial] :
      `(` ArgumentList[?Yield, ?Await, ~Partial] `)`
      `(` ArgumentList[?Yield, ?Await, ~Partial], `,` `)`
      [+Partial] [no LineTerminator here] `~` `(` ArgumentList[?Yield, ?Await, +Partial] `)`
      [+Partial] [no LineTerminator here] `~` `(` ArgumentList[?Yield, ?Await, +Partial] `,` `)`
    
    ArgumentList[Yield, Await, Partial] :
      AssignmentExpression[+In, ?Yield, ?Await]
      `...` AssignmentExpression[+In, ?Yield, ?Await]
      ArgumentList[?Yield, ?Await, ?Partial] `,` AssignmentExpression[+In, ?Yield, ?Await]
      ArgumentList[?Yield, ?Await, ?Partial] `,` `...` AssignmentExpression[+In, ?Yield, ?Await]
      [+Partial] `?` DecimalIntegerLiteral?
      [+Partial] `...`
      [+Partial] ArgumentList[?Yield, ?Await, ?Partial] `,` `?` DecimalIntegerLiteral?
      [+Partial] ArgumentList[?Yield, ?Await, ?Partial] `,` `...`

    注意:一个部分调用有多个 ... 占位符是 SyntaxError

    示例

    带时间戳的日志记录

    const log = console.log~({ toString() { return `[${new Date().toISOString()}]` } }, ?);
    log("test"); // [2018-07-17T23:25:36.984Z] test

    事件处理器

    button.addEventListener("click", this.onClick~(?));

    绑定方法

    class Collator {
      constructor() {
        this.compare = this.compare~(?, ?);
      }
      compare(a, b) { ... }
    }

    通过回调传递状态

    // doWork 期望一个 `(err, value) => void` 的回调
    function doWork(callback) { ... }
    function onWorkCompleted(err, value, state) { ... }
    doWork(onWorkCompleted~(?, ?, { key: "value" }));

    取消绑定 this

    const slice = Array.prototype.slice.call~(?, ?, ?);
    slice({ 0: "a", 1: "b", length: 2 }, 1, 2); // ["b"]

    您还可以在 EXAMPLES.md 中找到一些脱糖示例。

    与其他提案/语言特性的关系

    部分应用与管道

    管道提案 最近使用了 Hack 风格推进到第 2 阶段。虽然部分应用旨在与 F# 风格的管道配合使用,但最近的这一变化并未削弱部分应用的价值。实际上,转向 Hack 风格缓解了部分应用_不能有前缀标记_的要求,这是 TC39 某些成员提出的一个阻碍性问题。话虽如此,部分应用在与管道结合时仍有其用武之地:

    const add = (x, y) => x + y;
    const greaterThan = (x, y) => x > y;
    
    // 使用 Hack 风格管道
    elements
      |> map(^, add~(?, 1))
      |> filter(^, greaterThan~(?, 5));

    这在 Hack 风格管道中的主题变量(目前是 ^,虽然尚未最终确定)、部分调用(~())和占位参数(?)之间建立了视觉区分,这有助于提高可读性,并提高开发人员对其代码将如何求值的直觉。

    部分应用与可选链

    部分应用在_可选链_中受支持,参见上面的语义语法部分。由于部分应用与_参数_相关联,~( 调用约定会在可选调用中跟在 ?. 后面:

    const maybeAddOne = add?.~(?, 1); // undefined | Function
    const maybeLog = console?.log~(?); // undefined | Function

    根据_可选链_的语义,在上面的两个示例中,?. 标记都会短路链的其余部分的求值。因此,如果_被调用者_为 nullish,则两个表达式的结果都为 undefined。如果_被调用者_不为 nullish,则结果将是_被调用者_的部分应用。

    未决问题/关注点

    选择不同于 ? 的标记

    考虑到可选链可能使用 ?.,空值合并可能使用 ??,有人建议考虑使用除 ? 之外的另一个标记。我们认为这样的标记更改是不必要的,因为 ? _只能_在参数列表中单独使用,并且_不能_与这些运算符组合(例如 f~(??.a ?? c) 不合法)。? 标记的视觉效果与本提案最契合,而且今天使用现有标记来编写类似复杂的表达式也比较容易(例如 f(+i+++j-i---j)f([[][]][[]]))。在大多数情况下,一个包含部分应用、可选链和空值合并的有效且清晰的示例实际上并不难阅读:f~(?, a?.b ?? c)

    定义

    • 部分应用 — 一个带有零个或多个_占位参数_的调用或 new 表达式,其中_应用表达式_会立即求值并_固定_在调用中的各自位置。
      一个_部分应用_由 ~() 包围的参数列表表示。
      示例:f~()
    • 部分应用函数 — 作为_部分应用_结果的函数。
    • 应用表达式部分应用_中的_被调用者接收者_以及任何_非占位参数
      亦称:已应用
      反义词:未应用
    • 被调用者 — 要调用的函数或方法的值(对于调用)或要实例化的值(对于 new 表达式)。
    • 接收者 — 如果_被调用者_是一个方法调用,则_接收者_是作为其 this 绑定传递给调用的对象。
    • 固定 — 立即求值一个表达式并将其位置存储在_部分应用_中。
      亦称:固定的固定中
    • 非占位参数 — 占据整个参数位置的_应用表达式_。
    • 占位参数 — 在_部分应用_中尚未_应用_的参数。一个_占位参数_会在结果_部分应用函数_中产生一个或多个参数绑定。
    • 非序数占位参数 — 表示单个未应用参数的_占位参数_。_非序数占位参数_隐式地从左到右依次排序。
      一个_非序数占位参数_由占据整个参数位置的 ? 标记表示。
      示例:f~(?)
    • 序数占位参数 — 表示在结果_部分应用函数_的参数列表中具有指定序号位置的单个未应用参数的_占位参数_。
      一个_序数占位参数_由 ? 标记后跟一个表示结果参数序号位置的无符号整数表示。
      示例:f~(?1, ?0)
    • 剩余占位参数 — 表示传递给结果_部分应用函数_的任何多余参数的_占位参数_。
      一个_剩余占位参数_由占据整个参数位置的 ... 标记表示。
      示例:f~(...)

    资源

    TODO

    以下是推进 TC39 提案流程每个阶段的高级任务列表:

    第 1 阶段进入标准

    • 确定一位将推进该补充的"冠军"。
    • 概述问题或需求以及解决方案大致形态的散文
    • 说明性示例
    • 高级 API (提案不引入 API)

    第 2 阶段进入标准

    第 3 阶段进入标准

    第 4 阶段进入标准

    • 已为主流使用场景编写 Test262 验收测试并合并
    • 两个通过验收测试的兼容实现:[1][2]
    • 已向 tc39/ecma262 发送包含集成规范文本的拉取请求
    • ECMAScript 编辑器已签署拉取请求