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

    提案概览
    提案速览

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

    Note

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

    Map.prototype.getAndDelete & WeakMap.prototype.getAndDelete

    一份提案,旨在为 Map 和 WeakMap 添加 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. 每个人都在重复实现它。 这个辅助函数在不同的代码库中被以十几种名称重新发明(例如 getAndDelete、getAndRemove、pop、pull、popEntry、take、fetch、等等)。

    提案

    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无
    WeakMap 且 key 不能被弱持有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.get 和 Array.prototype.find 也有类似行为。

    早先的 getOrInsert 讨论 也认为 undefined 是可以接受的,因为 JavaScript 没有 Option 或 Maybe 类型,而且 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 } 的形状,当 found 为 false 时,value 为 undefined。

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

    带标记的结果也已被迭代器 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 没有那里讨论的 Box 或 Option 类型,因此 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) -> bool✅out 重载(在 .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() -> element✅JS 现有的移除并返回:弹出最后一个元素并返回它(或 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 命名了它组合的两个现有操作,并扩展了 get、getOrInsert、getOrInsertComputed 等家族。读取和修改的顺序是明确且可 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.get 和 Array.prototype.find 中已存在的歧义对 getAndDelete 来说是否可以接受?
    • 如果不能接受,getAndDelete 应该接受回退值,还是始终返回带标记的结果?
    • 这个草案返回 undefined,与 get 保持一致,因此 getAndDelete 可以作为 get 和 delete 这一组合的直接替代品,但另一种方案可以是 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);
    })();