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-Map-getAndDelete.md.
  • 简体中文
  • Map get and delete S1

    中文标题:Map 的 get 与 delete

    提案概览
    提案速览

    该提案为 MapWeakMap 添加 getAndDelete(key),将读取值和移除条目合并为单一操作。它返回被移除的值(如果键不存在则返回 undefined),取代了当前需要两次查找且容易出错的 get + delete 两步模式。该提案还讨论如何处理缺失键与值为 undefined 的存在键之间的歧义,考虑了直接返回值、可选回退值或带标记结果等方案。

    Note

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

    Map.prototype.getAndDelete & WeakMap.prototype.getAndDelete

    一份提案,旨在为 MapWeakMap 添加 getAndDelete(key) 方法,该方法在一个操作中移除 key 对应的条目,并返回其(如果键不存在则返回 undefined)。

    • 阶段: 1
    • 提案负责人: Devin Rousso
    • 作者: Devin Rousso

    动机

    “读取一个值并在同一步中将其移除”是人们对 map 最常见的操作之一(例如,使用以请求 ID 为键的待处理回调、从工作队列中弹出一个任务、逐出并检查缓存条目、移交缓冲块的所有权、清空“等待 X”表等)。

    JavaScript 可以将 CRUD 四元组中的其他每个成员表达为返回有用结果的单次调用(例如 map.get(k)map.set(k, v)(返回 map)和 map.has(k)),唯独“移除并读取”这一组合除外。

    Map.prototype.delete 返回一个布尔值,告诉你是否有任何内容被移除(即它会丢弃这个值)。

    因此今天你必须手动编写保留值的版本,这需要对同一个键进行两次查找

    function getAndDelete(map, key) {
      const value = map.get(key); // lookup #1
      map.delete(key);            // lookup #2
      return value;
    }

    这有实际的缺点:

    1. 两次查找而不是一次。 在热路径(调度表、逐帧缓存等)上,额外的查找纯粹是开销,如果该操作是一个单一方法,引擎本可以避免。
    2. 内联时容易出错。 const v = map.get(k); map.delete(k); return v; 很容易被错误地重排(例如在捕获值之前执行 delete),而且 delete 的布尔结果会被静默丢弃,因此 linter 也无法提供帮助。
    3. 每个人都在重复实现它。 这个辅助函数在不同的代码库中被以十几种名称重新发明(例如 getAndDeletegetAndRemovepoppullpopEntrytakefetch、等等)。

    提案

    Map.prototype.getAndDelete(key)     // the removed value, or undefined if key was absent
    WeakMap.prototype.getAndDelete(key) // the removed value, or undefined if key was absent

    getAndDelete(key) 被定义为在可观察行为上等同于先读取 get(key) 再执行 delete(key),并返回 get 原本会返回的值。

    它会原地修改 map 并直接返回值(即不会返回 map),因为重点在于把值交还给调用者。

    示例

    // Consume request.
    const resolveForRequestId = new Map();
    function onResponse(id, payload) {
      const resolve = resolveForRequestId.getAndDelete(id); // grab it and clear it in one step
      resolve?.(payload);
    }
    
    // Move ownership.
    const buffers = new Map();
    // ...
    const chunk = buffers.getAndDelete(streamId); // caller now owns `chunk`
    
    // One-off logic.
    const stateForObject = new WeakMap();
    function consume(obj) {
      const state = stateForObject.getAndDelete(obj);
      return state;
    }

    语义

    情况getAndDelete(key) 返回副作用
    key 存在且值为 vv条目被移除
    key 存在且值为 undefinedundefined条目被移除
    key 不存在undefined
    WeakMapkey 不能被弱持有undefined
    this 不是 Map/WeakMap抛出 TypeError

    get 一样,返回 undefined 在“不存在”和“存在但值为 undefined”之间是有歧义的。

    get 不同,调用者在收到 undefined 后无法通过调用 has(key) 来解决这种歧义,因为 getAndDelete 已经移除了匹配的条目。

    预先调用 has(key) 可以,但需要第二次查找。

    getAndDelete 是否应该提供一种用一次查找区分这些情况的方法,仍是下文中的一个开放问题。

    区分缺失键与 undefined

    Map 可以存储任何 ECMAScript 语言值,包括任何 Symbol 或对象,API 可以将它们作为缺失值的哨兵值暴露出来。

    如果 getAndDelete 必须原样返回每个存在的值,那么语言提供的任何哨兵值都无法避免所有冲突,因为该哨兵值本身也可能被存储在 map 中。

    同样的问题也出现在 getIntrinsic 提案 中。任何哨兵值也都可能是有效结果,但使用容器会带来更差的人体工程学体验。

    现有的两次调用形式既能保留被移除的值,也能保留键是否存在:

    const value = map.get(key);
    const found = map.delete(key);

    直接返回值只会将这种情况减少为一次查找,前提是调用者不需要 found

    因此,TC39 似乎可能会考虑三个选项。

    1. 继续直接返回值

    该提案可以保持当前行为,并要求需要区分情况的调用者先调用 has

    if (map.has(key)) {
      const value = map.getAndDelete(key);
      use(value); // `value` may be undefined
    } else {
      handleMissing();
    }

    这将保持 API 的小巧,避免创建结果对象,并继续直接返回被移除的值。

    它也将遵循现有 JavaScript API 的做法。

    Array.prototype.pop 也有同样的歧义,因为空数组和最后一个值为 undefined 的数组都会产生 undefined

    Map.prototype.getArray.prototype.find 也有类似行为。

    早先的 getOrInsert 讨论 也认为 undefined 是可以接受的,因为 JavaScript 没有 OptionMaybe 类型,而且 Map.prototype.get 已经以这种方式工作。

    缺点是需要区分情况的调用者仍然要执行两次查找。

    Array.prototype.pop 的先例没有同样的性能成本,因为检查 array.length 不会重复查找元素,而调用 map.has(key) 会重复对 key 的查找。

    这可能是最有可能被接受的选项,因为它与直接先例一致,并且不会为每个调用者增加复杂性。

    2. 可选的回退值

    getAndDelete 可以接受第二个值,仅在键不存在时返回该值:

    const missing = Symbol("missing");
    const value = map.getAndDelete(key, missing);
    
    if (value === missing)
      handleMissing();
    else
      use(value); // `value` may be undefined

    使用一个参数调用 getAndDelete(key) 时,如果键不存在,将继续返回 undefined

    这将保留直接返回值,只需要一次查找,并避免创建结果对象。

    可以创建一次私有 Symbol 并重复使用,也可以使用普通值作为默认值。

    主要问题在于,调用者必须预留一个 map 不会包含的值,才能保证区分。

    默认值也会在方法运行之前被求值,即使键存在时也是如此。

    如果委员会想要一种用一次查找区分这些情况的方法,这可能是最合理的补充。

    它沿用了 Python 的 dict.pop 以及第 4 阶段 Map.prototype.getOrInsert 的默认值形式。

    早先的 Map.prototype.getWithDefault 讨论 也认识到 map.get(key) ?? fallback 并不等价,因为值为 undefined 的存在键不能使用回退值。

    Map.prototype.getOrInsert 现在在内部做了同样的区分,返回存在的 undefined 值,并且仅在键不存在时使用提供的值。

    3. 始终返回带标记的结果

    该方法可以改为返回包含这两条信息的对象:

    const { found, value } = map.getAndDelete(key);
    
    if (found)
      use(value); // `value` may be undefined
    else
      handleMissing();

    例如,结果可以始终具有 { found, value } 的形状,当 foundfalse 时,valueundefined

    这将始终区分这些情况,保留所有存储的值,并且只需要一次查找。

    带标记的结果也已被迭代器 API 和 Promise.allSettled 使用,因此这种形状对 JavaScript 来说并不陌生。

    一个相关版本可以使用包装器本身来表示键存在:

    const result = map.getAndDelete(key);
    
    if (result)
      use(result.value); // `result.value` may be undefined
    else
      handleMissing();

    这个版本会在键存在时返回 { value },否则返回 undefined。它会在键不存在时避免创建对象,但仍然会包装每个被移除的值。

    Object.getOwnPropertyDescriptor 类似地在属性存在时返回对象,否则返回 undefined。TC39 还在 Array.prototype.find 中讨论过这种方法,成功的返回结果可以包装任何值(包括 undefined),而缺失的结果仍为 undefined

    JavaScript 没有那里讨论的 BoxOption 类型,因此 getAndDelete 需要创建自己的结果对象。

    缺点是每次调用都需要解包结果,而不是直接使用被移除的值。

    它也会为每次调用创建一个结果对象。

    getOrInsert 讨论 提出了类似的对高频调用的 map 代码中分配内存的担忧,其最终设计移除了选项对象。

    该讨论涉及的是输入对象,而不是返回的结果对象,因此它并不能决定这个问题。但它确实表明,高频调用的 map 操作中的内存分配很可能受到审视。

    由于避免开销是 getAndDelete 的主要动机之一,这将是一个显著的代价。

    getOrInsert 的早期版本也曾尝试在单一 API 中处理插入和更新回调。

    评审反馈 认为该 API 因为试图做太多事情而不直观,而现在第 4 阶段的设计则专注于常见操作。

    基于这段历史,该提案应该聚焦于这三个选项,而不是试图涵盖所有可能的返回状态的方式。

    已有做法

    “移除并返回值”是大多数语言标准库中此操作的默认形态。

    JavaScript 是例外,它只从 delete 返回布尔值。

    语言API移除并返回值?备注
    RustHashSet::take(&value) -> Option<T>名称 take 的直接先例:“移除并返回集合中的值(如果有的话)”。
    RustHashMap::remove(&k) -> Option<V> / remove_entry返回值(或 (k, v) 对)。
    Pythondict.pop(key[, default])返回值;如果不存在且未给出默认值则抛出 KeyError
    RubyHash#delete(key)返回值(或 nil);缺失情况下可使用可选块。
    JavaMap.remove(Object key) -> V返回先前的值,或 null
    C#Dictionary.Remove(key, out value) -> boolout 重载(在 .NET Core 3.0 中新增)会交回被移除的值。
    SwiftDictionary.removeValue(forKey:) -> Value?返回被移除的值,或 nil
    KotlinMutableMap.remove(key): V?返回先前的值,或 null
    C++std::map::extract / unordered_map::extract (C++17)移除并交回一个拥有键/值的节点句柄。
    Perldelete $hash{$key}delete 运算符返回被删除的值。
    Godelete(m, k)无返回值;你必须编写 v, ok := m[k]; delete(m, k) —— 也就是 JavaScript 不得不采用的同样的两步操作。
    JavaScriptArray.prototype.pop() -> elementJS 现有的移除并返回:弹出最后一个元素并返回它(或 undefined)。take 是 map 的按对应操作。
    JavaScript (today)Map.prototype.delete -> boolean仅返回是否有内容被移除;值会被丢弃。

    先例

    getAndDelete 对 JavaScript 来说并不陌生,因为该语言在 Array.prototype.pop 中已经有了一个“移除元素并返回其值”的方法。

    它会原地修改集合,返回被移除的元素,并在没有可移除内容时产生 undefined

    getAndDelete 将这个确切的契约应用于键控集合(即按键而不是按位置寻址):

    Array.prototype.pop()Map.prototype.getAndDelete(key)
    选择要移除的内容依据位置(最后一个索引,LIFO)
    参数
    返回被移除的元素被移除的值
    没有匹配项时undefined(空数组)undefined(键不存在)
    原地修改
    返回容器以便链式调用❌(返回元素)❌(返回值)

    由于 pop 已经将“修改并交回值”确立为一种惯用的 JavaScript 模式,getAndDelete 是它的一种小而一致的扩展,而不是一个新概念(即它只是按键移除)。

    命名

    • getAndDelete 命名了它组合的两个现有操作,并扩展了 getgetOrInsertgetOrInsertComputed 等家族。读取和修改的顺序是明确且可 grep 的。
    • take 与 Rust 的 HashSet::take(“移除并返回”)一致,并且读起来自然地像是“从 map 中取出值”。
    • pop 具有误导性,因为 Array.prototype.pop 不接受参数,并且从末尾(即 LIFO)移除,而不是按索引移除。
    • remove 会与现有的 delete 并排造成混淆(即两种拼写具有不同的返回类型),并且与许多用户态的 remove 方法冲突。

    该提案在推进到第 1 阶段时采用了 getAndDelete

    使用情况

    以下计数来自 GitHub GET /search/code,它基于 token 索引,仅覆盖公共仓库的默认分支,并且不会验证匹配到的 token 是否作用于同一个键/变量。

    查询文件数
    "getAndDelete(" language:TypeScript~260
    "getAndDelete(" language:JavaScript~281
    "popEntry(" in:file (JS/TS)~3,270
    getAndRemove in:file (所有语言,token 匹配)~48,000
    查询文件数
    cache.get cache.delete language:TypeScript~143,000
    map.get map.delete language:JavaScript~99,800
    "get(key)" "delete(key)" language:TypeScript~44,600

    示例

    // pull out the pending request/callback when its response arrives
    let responseData = this._pendingResponses.getAndDelete(sequenceId) || {};
    
    // remove and read a worker as it goes away
    let worker = this._workerForId.getAndDelete(workerId);
    
    // adopt resources that were buffered for a target
    let resources = this._orphanedResources.getAndDelete(target.identifier);

    问题

    • Map.prototype.getArray.prototype.find 中已存在的歧义对 getAndDelete 来说是否可以接受?
    • 如果不能接受,getAndDelete 应该接受回退值,还是始终返回带标记的结果?
    • 这个草案返回 undefined,与 get 保持一致,因此 getAndDelete 可以作为 getdelete 这一组合的直接替代品,但另一种方案可以是 Python 风格的可选默认值 getAndDelete(key, defaultValue)
    • 是否值得为了对齐而添加 Set.prototype.take 和/或 WeakSet.prototype.take,就像 Set.prototype.entries 返回二元数组以匹配 Map.prototype.entries 那样?

    Polyfill

    'use strict';
    
    (function installMapGetAndDeletePolyfill() {
      function defineGetAndDelete(Ctor) {
        if (typeof Ctor !== 'function' || typeof Ctor.prototype.getAndDelete === 'function')
            return;
    
        const get = Ctor.prototype.get;
        const del = Ctor.prototype.delete;
    
        function getAndDelete(key) {
          const value = get.call(this, key);
          del.call(this, key);
          return value;
        }
    
        Object.defineProperty(Ctor.prototype, 'getAndDelete', {
          value: getAndDelete,
          writable: true,
          enumerable: false,
          configurable: true,
        });
      }
    
      defineGetAndDelete(typeof Map === 'function' ? Map : undefined);
      defineGetAndDelete(typeof WeakMap === 'function' ? WeakMap : undefined);
    })();