Map.prototype.keys() 返回的迭代器是“快照式”的,创建时固化当前键的插入顺序,后续增删不影响已生成迭代器;但遍历中修改 Map 会导致未定义行为,不同引擎表现不一致。

Map.prototype.keys() 返回的迭代器是“快照式”的,它在创建时捕获 Map 当前的键序列,后续对 Map 的增删操作不会影响已生成的迭代器行为——但这个“快照”并不意味着完全静态安全,尤其在遍历过程中修改 Map 会触发 未定义行为(undefined behavior),而非抛错或自动同步。
什么是 keys() 迭代器的“快照”本质
调用 map.keys() 时,引擎会记录当前 Map 中所有键的插入顺序(按添加时间排序),并固化为一个只读序列。该序列不随后续 set()、delete() 或 clear() 动态更新。
- 新插入的键一定不会出现在已创建的迭代器中(即使在
next()调用前插入) - 已删除的键如果在快照中存在,仍会被迭代器返回(除非该键在迭代器创建前已被删)
- 但若在
for...of循环中边遍历边修改 Map,规范未保证结果一致——不同 JS 引擎可能表现不同
遍历中增删键的典型后果
虽然不报错,但行为不可靠:
-
新增键被忽略:在
for (const key of map.keys())中执行map.set('new', 1),'new' 永远不会被本轮循环访问到 - 删除正在遍历的键可能跳过下一个键:V8 曾出现过因内部哈希表重散列导致跳项;SpiderMonkey 和 JavaScriptCore 也有类似风险
-
重复访问已删除键:若某键在迭代器快照中排第 3 位,你在访问第 2 个键后删掉它,第 3 次
next()仍返回该键(因为快照里它还在)
如何识别这类问题
不是靠运行时错误,而是通过逻辑矛盾和测试暴露:
- 遍历长度与
map.size不一致(如遍历时map.size增加,但迭代器总项数不变) - 预期被跳过的键实际出现了,或预期出现的键消失了(尤其在并发修改或异步回调中)
- 在不同浏览器或 Node.js 版本下输出顺序/内容不一致(例如 Chrome 正常,Safari 报错或漏项)
- 使用
console.log([...map.keys()])和for...of输出不同结果(前者是即时展开,后者是惰性迭代)
安全替代方案
避免在迭代期间修改原 Map:
- 先收集要操作的键:
const keysToDel = [...map.keys()].filter(...); keysToDel.forEach(k => map.delete(k)) - 用
Array.from(map)获取键值对快照,再处理 - 需要实时响应变化?改用
WeakMap+ 外部状态管理,或封装成支持事件的自定义 Map 类 - 调试时可用
map.forEach((v, k) => {...})—— 它也是基于快照,但语义更明确,不易误增删
快照隔离不是保护机制,而是实现约定。它不阻止你出错,只让错误更难复现和调试。

















