Map get and delete S1
中文标题:Map 的 get 与 delete
- 阶段: Stage 1
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案为 Map 和 WeakMap 添加 getAndDelete(key),将读取值和移除条目合并为单一操作。它返回被移除的值(如果键不存在则返回 undefined),取代了当前需要两次查找且容易出错的 get + delete 两步模式。该提案还讨论如何处理缺失键与值为 undefined 的存在键之间的歧义,考虑了直接返回值、可选回退值或带标记结果等方案。
以下 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 返回一个布尔值,告诉你是否有任何内容被移除(即它会丢弃这个值)。
因此今天你必须手动编写保留值的版本,这需要对同一个键进行两次查找:
这有实际的缺点:
- 两次查找而不是一次。 在热路径(调度表、逐帧缓存等)上,额外的查找纯粹是开销,如果该操作是一个单一方法,引擎本可以避免。
- 内联时容易出错。
const v = map.get(k); map.delete(k); return v;很容易被错误地重排(例如在捕获值之前执行delete),而且delete的布尔结果会被静默丢弃,因此 linter 也无法提供帮助。 - 每个人都在重复实现它。 这个辅助函数在不同的代码库中被以十几种名称重新发明(例如
getAndDelete、getAndRemove、pop、pull、popEntry、take、fetch、等等)。
提案
getAndDelete(key) 被定义为在可观察行为上等同于先读取 get(key) 再执行 delete(key),并返回 get 原本会返回的值。
它会原地修改 map 并直接返回值(即不会返回 map),因为重点在于把值交还给调用者。
示例
语义
与 get 一样,返回 undefined 在“不存在”和“存在但值为 undefined”之间是有歧义的。
与 get 不同,调用者在收到 undefined 后无法通过调用 has(key) 来解决这种歧义,因为 getAndDelete 已经移除了匹配的条目。
预先调用 has(key) 可以,但需要第二次查找。
getAndDelete 是否应该提供一种用一次查找区分这些情况的方法,仍是下文中的一个开放问题。
区分缺失键与 undefined 值
Map 可以存储任何 ECMAScript 语言值,包括任何 Symbol 或对象,API 可以将它们作为缺失值的哨兵值暴露出来。
如果 getAndDelete 必须原样返回每个存在的值,那么语言提供的任何哨兵值都无法避免所有冲突,因为该哨兵值本身也可能被存储在 map 中。
同样的问题也出现在 getIntrinsic 提案 中。任何哨兵值也都可能是有效结果,但使用容器会带来更差的人体工程学体验。
现有的两次调用形式既能保留被移除的值,也能保留键是否存在:
直接返回值只会将这种情况减少为一次查找,前提是调用者不需要 found。
因此,TC39 似乎可能会考虑三个选项。
1. 继续直接返回值
该提案可以保持当前行为,并要求需要区分情况的调用者先调用 has:
这将保持 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 可以接受第二个值,仅在键不存在时返回该值:
使用一个参数调用 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. 始终返回带标记的结果
该方法可以改为返回包含这两条信息的对象:
例如,结果可以始终具有 { found, value } 的形状,当 found 为 false 时,value 为 undefined。
这将始终区分这些情况,保留所有存储的值,并且只需要一次查找。
带标记的结果也已被迭代器 API 和 Promise.allSettled 使用,因此这种形状对 JavaScript 来说并不陌生。
一个相关版本可以使用包装器本身来表示键存在:
这个版本会在键存在时返回 { 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 返回布尔值。
先例
getAndDelete 对 JavaScript 来说并不陌生,因为该语言在 Array.prototype.pop 中已经有了一个“移除元素并返回其值”的方法。
它会原地修改集合,返回被移除的元素,并在没有可移除内容时产生 undefined。
getAndDelete 将这个确切的契约应用于键控集合(即按键而不是按位置寻址):
由于 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 是否作用于同一个键/变量。
示例
问题
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那样?