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/proposal-discard-binding.md.
  • 简体中文
  • "Discard" (void) Bindings S2

    中文标题:ECMAScript 的 void 丢弃绑定

    提案概览
    提案速览

    该提案在 ECMAScript 中引入 void 丢弃绑定,允许忽略值而无需声明未使用的绑定,涵盖变量声明、解构模式、参数、提取器和模式匹配。它解决了显式省略值和避免临时变量或下划线前缀变量的问题,语法如 using void = .const { x: void } = obj(void, i) => i

    Note

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

    ECMAScript 的 void 丢弃绑定

    本提案旨在将 void 作为“丢弃”绑定引入 ECMAScript,用于替代众多结构中的 BindingIdentifierElision

    状态

    阶段: 2
    推动者: Ron Buckton (@rbuckton)
    上次呈现:(无)

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

    作者

    • Ron Buckton (@rbuckton)

    概述和动机

    从 2018 年开始,显式资源管理 提案曾计划包含一种无绑定形式,允许你进入一个资源管理作用域,该作用域针对的是预先存在的可处置资源实例或不需要用户可引用引用的实例,例如互斥锁上的锁。在功能于 Stage 2 期间被移除之前,它的形式大致如下:

    {
      // lock 被初始化并跟踪以便处置,但不声明任何绑定
      using void = new UniqueLock(mutex);
    
      ...
    
    } // lock 被释放

    这种无绑定形式的想法被移除并不是因为它没有用或不需要,而是因为“丢弃”绑定有更广泛的用例,这些用例不仅仅局限于 using 声明,并且值得单独提出完整提案。

    本提案旨在研究“丢弃”绑定的许多潜在用例:

    先前艺术

    语法

    虽然大多数具有丢弃概念的语言使用下划线(_)来表示丢弃,但 _ 字符在 ECMAScript 中是有效的 Identifier。过去的讨论表明,重新利用任何有效的现有 Identifier 存在很大的阻力,并且这样的更改很可能成为对语言不兼容的破坏性更改。

    相反,本提案希望采用 void 关键字用于此目的,因为当与表达式一起使用时,它大致具有类似的语义,其中 void foo() 会执行 foo() 并丢弃返回值。在赋值模式的情况下,裸 void 可能需要覆盖文法或静态语义规则,才能在不属于赋值时与普通 void 表达式区分开来。

    变量声明

    using 声明

    在最简单的形式中,void 丢弃绑定可以在任何变量声明中用作 BindingIdentifier 的替代。通常,这主要对 usingawait using 声明有用:

    using void = new UniqueLock(mutex);

    var/let/const 声明

    丢弃绑定不支持出现在 var/let/const 声明的顶层。

    const void = bar(); // 语法错误

    对象绑定和赋值模式

    对于对象绑定模式,丢弃绑定具有允许开发者明确地从剩余绑定(...)中排除属性,而无需为此引入一次性临时变量的好处:

    const { z: void, ...obj1 } = { x: 1, y: 2, z: 3 };
    obj1; // { x: 1, y: 2 }
    
    let obj2;
    ({ z: void, ...obj2 } = { x: 1, y: 2, z: 3 });
    obj2; // { x: 1, y: 2 }

    数组绑定和赋值模式

    数组模式中的丢弃绑定提供了两种有用的能力,因为它们可以充当更明确的省略指示符,并且可以帮助避免尾随逗号混淆:

    function* gen() {
      for (let i = 0; i < Number.MAX_SAFE_INTEGER; i++) {
        console.log(i);
        yield i;
      }
    }
    
    const iter = gen();
    const [a, , ] = iter;
    // 打印:
    //  0
    //  1

    在这里,读者不清楚作者是有意从生成器中消费两个还是三个元素,因此这可能是一个错误。丢弃绑定有助于使意图更加明确:

    const [a, void] = iter; // 作者打算消费两个元素
    // 相对于
    const [a, void, void] = iter; // 作者打算消费三个元素

    参数

    参数声明中的 void 丢弃绑定有助于避免为可能未被回调或子类的重写方法使用的参数命名:

    // 将数组值投影为索引数组
    const indices = array.map((void, i) => i);
    
    // 向 `Map.prototype.forEach` 传递仅关心键的回调
    map.forEach((void, key) => { });
    
    // 监视特定的已知文件以获取事件
    fs.watchFile(fileName, (void, kind) => { });
    
    // 忽略重写方法中未使用的参数
    class Logger {
      log(timestamp, message) {
        console.log(`${timestamp}: ${message}`);
      }
    }
    
    class CustomLogger extends Logger {
      log(void, message) {
        // 这个日志器不使用时间戳...
      }
    }

    虽然简单的情况可以使用像 _ 这样的标识符,但处理多个被丢弃的参数时,这变得更加繁琐:

    doWork((_, a, _1, _2, b) => {});
    // 相对于
    doWork((void, a, void, void, b) => {
    });

    提取器

    提取器 提案目前不包含 void 丢弃,并打算依赖本提案来引入它们。在提取器的上下文中,void 丢弃绑定将具有与为数组绑定模式提出的语法类似的语法:

    const msg = new Message("subject", "body");
    const Message(void, body) = msg;

    提取器还将在嵌套的对象绑定模式和数组绑定模式中利用丢弃:

    const IsoDate = /^(\d{4})-(\d{2})-(\d{2})$/;
    const IsoDate([void, year, month, day]) = text;

    模式匹配

    模式匹配 提案有兴趣引入“丢弃模式”,这是不可反驳的模式(例如,它们总是匹配)。丢弃模式对于匹配对象上的特定属性而无需匹配特定值或依赖自定义匹配器非常有用:

    match (shape) {
      when { x: void, y: void }: drawPoint(shape);
      when { p1: void, p2: void }: drawLine(shape);
      when { p: void, r: void }: drawCircle(shape);
      when { p: void, r1: void, r2: void }: drawEllipse(shape);
      when { p: void, h: void, w: void }: drawRectangle(shape);
    }

    应该注意的是,模式匹配提案的推动者在没有解构中一致的丢弃语法的情况下,对包含丢弃模式持保留态度,因此本提案旨在解决语言中丢弃的横切问题。

    其他形式

    catch 子句

    本提案不积极追求 catch 子句的丢弃绑定,因为无绑定的 catch 已经存在,并且它的采用没有面临其他无绑定形式所面临的复杂性,因为 catch 可以简单地省略子句的 () 部分。

    导入和导出

    本提案不积极追求导入或导出的丢弃绑定,因为似乎没有包含它们的任何价值。无绑定的 import 已经存在(即,import "module"),并且没有“剩余”(...)导入或重新导出的概念,因此需要丢弃绑定来省略特定的命名导入或导出。

    函数和类名

    本提案不积极追求函数或类名的丢弃绑定,因为这些语法形式已经通过简单省略标识符定义了丢弃绑定的明确语法。

    语义

    void 丢弃绑定不会在当前环境记录的 var 作用域名称或词法声明的名称中引入新绑定。但是,这些绑定中的相关初始化器仍将被求值,并且对于 usingawait using 声明,将被跟踪以供将来清理。

    赋值模式中的 void 丢弃将执行与相同位置 IdentifierReference 相同的算法步骤,只是不会进行赋值。

    模式匹配中的 void 丢弃将始终成功。

    文法

    请参阅规范文本以获取本提案的正式文法。

    拟议的文法旨在涵盖以下示例:

    var [void] = x;         // 通过:BindingPattern :: `void`
    var {x:void};           // 通过:BindingPattern :: `void`
    
    let [void] = x;         // 通过:BindingPattern :: `void`
    let {x:void};           // 通过:BindingPattern :: `void`
    
    const [void] = x;       // 通过:BindingPattern :: `void`
    const {x:void} = x;     // 通过:BindingPattern :: `void`
    
    function f(void) {}     // 通过:BindingPattern :: `void`
    function f([void]) {}   // 通过:BindingPattern :: `void`
    function f({x:void}) {} // 通过:BindingPattern :: `void`
    
    ((void) => {});         // 通过:BindingPattern :: `void`
    (([void]) => {});       // 通过:BindingPattern :: `void`
    (({x:void}) => {});     // 通过:BindingPattern :: `void`
    
    using void = x;         // 通过:LexicalBinding : `void` Initializer
    await using void = x;   // 通过:LexicalBinding : `void` Initializer
    
    [void] = x;             // 通过:DestructuringAssignmentTarget : `void`
    ({x:void} = x);         // 通过:DestructuringAssignmentTarget : `void`

    注意:

    void = x;

    是不允许的,因为 void 不是 LeftHandSideExpressionAssignmentPattern 的限制的一部分,参见 13.15.5 解构赋值

    另外注意:

    const x = void;

    是不允许的,因为 void 不是 CoverVoidExpressionAndVoidDiscard 的限制的一部分。

    注意:LexicalBinding 文法是对 显式资源管理 拟议文法的差异,如这里的提案差异所示:https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-let-const-and-using-declarations

    常见问题解答

    问:{} 难道不够吗?例如,using {} = x 而不是 using void = x

    不,{} 不够,原因有很多。首先,using 声明不允许使用绑定模式,所以 {} 在该上下文中不合法。其次,{} 要求右侧既不是 null 也不是 undefined。不仅 using 声明明确允许将 nullundefined 用作资源,而且你可能想要从绑定中丢弃的任何值都可能为 nullundefined

    问:为什么使用 void 关键字?为什么不使用其他关键字或新令牌?

    ECMAScript 中的语法空间相当有限。许多类似运算符的令牌已经有明确定义的语义,与“丢弃”的概念不符。我们不能引入新关键字,因为它可能与现有标识符冲突,并且有公认的指导原则,即提案不应引入此类冲突。这给我们留下了一组有限的既有关键字,我们可以重新利用它们用于此用途,而不与它们的其他用途冲突。由于让关键字在所有上下文中保持一致的语义是一个良好的实践,我们选择了 void,因为它与 void 在语言中的现有使用方式保持一致。void 表达式 会求值其操作数并丢弃结果。void 绑定 会求值其 初始化器、从其关联属性读取、或从迭代器读取元素,然后_也_丢弃结果。

    话虽如此,我们欢迎符合这些原则的其他建议。

    问:为什么模式匹配需要丢弃?

    模式匹配的主要好处之一是能够根据输入的_形状_执行条件逻辑。例如:

    match (p) {
      when { x: number, y: Number }: print("p is a point");
    }

    在这个例子中,当 p 同时具有 xy 属性,并且这两个属性的值都是 Number 值时,模式匹配 p。但是,你如何在不关心类型的情况下测试属性是否存在?如果没有“丢弃”模式,你将面临一些糟糕的选择。

    你可以使用条件和其补集同时匹配一个属性:

    match (p) {
      when { x: null or not null, y: null or not null }: ...;
    }

    然而,这非常啰嗦且不易发现。

    你可以匹配一个属性并引入一个临时变量:

    match (p) {
      when { x: let _, y: let _ }: ...;
    }

    然而,这引入了另一个不必要的临时变量,并且同样不易发现。

    最后,你可以引入一个自定义匹配器:

    const Discard = {
      [Symbol.matcher](_value) {
        return true;
      }
    };
    
    match (p) {
      when { x: Discard, y: Discard }: ...;
    }

    然而,这引入了额外的开销,因为必须求值用户代码。

    相反,void 模式既易于发现,因为其语义一致,而且没有求值用户代码的运行时开销:

    match (p) {
      when { x: void, y: void }: ...;
    }

    由于模式匹配仍处于 Stage 1,它可能会选择使用 let 模式将 void 丢弃实现为纯绑定:

    match (p) {
      when { x: let void, y: let void }: ...;
    }

    无论哪种情况,语义含义都将保持一致。

    示例

    using 声明

    {
      using void = new UniqueLock(mutex); // 绑定否则将未被使用
      ...
    } // lock 被处置

    数组解构中 Elision 的显式替代

    const [, a, , b] = ar; // 使用省略跳过
    const [void, a, void, b] = ar; // 使用 `void` 跳过
    
    const [a, b, , ] = iter; // 从可迭代对象中消耗*三个*元素
    const [a, b, void] = iter; // 相同,但使用 `void`

    从对象解构的剩余 (...) 模式中省略属性

    const { z: void, ...obj } = { x: 1, y: 2, z: 3 };
    obj; // { x: y, y: 2 }

    提取器中的显式省略

    const Color(void, void, g) = c; // 跳过 r 和 b 分量

    模式匹配的丢弃模式

    match (obj) {
      when { x: void, y: void }: usePoint(obj);
    }

    相关提案

    待办事项

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

    Stage 1 准入标准

    • 确定一名“推动者”来推进该补充。
    • 概述问题或需求以及解决方案大致形态的[散文][]。
    • 说明性 示例
    • 高级 API

    Stage 2 准入标准

    Stage 2.7 准入标准

    Stage 3 准入标准

    Stage 4 准入标准

    • 两个兼容的实现通过验收测试:[1][2]
    • 已向 tc39/ecma262 发送拉取请求,其中包含集成的规范文本。
    • ECMAScript 编辑已签署拉取请求