踩坑实录:JS Map迭代器为什么“用完即焚”?兼谈技术知识沉淀

案发现场:第二次遍历怎么就"隐身"了?

前几天我在重构一段老代码时,遇到了一个让人极其挠头的 Bug。业务逻辑其实挺简单:用 Map 存了一批配置数据,我想先遍历一遍 keys() 做个前置校验,然后再遍历一遍执行具体的业务处理。

代码大概长这样:

javascript 复制代码
const myMap = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3]
]);

const keys = myMap.keys();

// 第一次遍历:做校验
for (const key of keys) {
  console.log('校验:', key);
}

// 第二次遍历:做业务
for (const key of keys) {
  console.log('处理:', key); // 这里居然没执行!
}

跑起来一看,控制台只打印了"校验",第二次循环直接静默了,仿佛 keys 里的数据凭空消失了。我当时第一反应是:难道我的 Map 被哪个异步操作给清空了?排查了半天,最后发现罪魁祸首竟然是 JavaScript 里的一个"隐形杀手"------迭代器对象。

根本原因:迭代器的"用完即焚"属性

很多人(包括曾经的我)会下意识地以为 Map.keys() 返回的是个数组,或者至少是个能反复读取的集合。毕竟平时用 Object.keys() 返回的就是个正儿八经的数组,想遍历几次就遍历几次。

Map 不一样。它的 keys()values()entries() 方法,返回的都是迭代器对象(Iterator)

在 JS 的底层设定里,迭代器是个"一次性消耗品"。它内部维护着一个指针,你每调用一次 next()(或者用 for...of 遍历),指针就往后挪一步。等遍历到尽头,返回 { done: true } 后,这个迭代器就彻底"耗尽"了。

你再去引用它?对不起,指针已经停在终点了,它再也吐不出任何新数据。这就是为什么第二次引用时,循环体根本不会执行。

划重点:Map 迭代器只能遍历一次,一旦耗尽,无法重置。

怎么破?两招搞定

既然摸清了它的脾气,对付起来就不难了。我总结了两个最常用的解法,大家可以根据实际场景按需取用。

1. 简单粗暴:转成数组

如果你的数据量不大,最直接、最省心的办法就是把它"拍扁"成数组。用扩展运算符 ... 或者 Array.from() 都可以。变成数组后,你想怎么遍历就怎么遍历。

javascript 复制代码
const myMap = new Map([['a', 1], ['b', 2]]);

// 用扩展运算符转成数组
const keysArray = [...myMap.keys()];

for (const key of keysArray) {
  console.log('校验:', key);
}

for (const key of keysArray) {
  console.log('处理:', key); // 完美执行
}

这种做法代码可读性最高,但代价是会在内存里额外创建一份数组副本。如果 Map 里有几十万条数据,这招可能会让你的内存飙升。

2. 按需生成:每次重新拿迭代器

如果数据量特别大,或者你对内存开销比较敏感,那就别缓存迭代器了。每次需要遍历的时候,重新调用一次 keys() 生成一个新的迭代器即可。

javascript 复制代码
const myMap = new Map([['a', 1], ['b', 2]]);

// 第一次遍历
for (const key of myMap.keys()) {
  console.log('校验:', key);
}

// 第二次遍历,重新调用 keys()
for (const key of myMap.keys()) {
  console.log('处理:', key);
}

这种做法没有额外的内存开销,但稍微牺牲了一点点代码的简洁度。不过权衡之下,在大数据量场景里,这绝对是更优雅的选择。

从踩坑到沉淀:技术笔记到底该写点啥?

解决完 Bug,故事其实还没完。

我见过不少开发者,踩完坑、改完代码,长舒一口气就切去打游戏了。但过几个月,换个项目,同样的坑他大概率还会再踩一次。这就是为什么我一直死磕技术写作与知识沉淀

写技术博客,从来不是为了凑字数拿全勤奖,而是给未来的自己(以及遇到同样问题的同行)留一份"避坑指南"。

像今天这个迭代器的坑,如果笔记里只干巴巴地写一句"Map 迭代器只能遍历一次",那太没营养了,过阵子你自己都看不懂。一份合格的知识沉淀,至少得包含这三个层次:

  • 还原案发现场:你是在什么业务场景下遇到的?贴出引发问题的代码。这能帮读者(包括未来的你)快速代入语境。
  • 深挖底层逻辑:为什么 JS 要这么设计?搞懂迭代器的指针机制,你以后遇到 Set、Generator 时就不会再犯同样的错。知其然,更得知其所以然。
  • 给出方案与权衡:转数组和重新生成各有什么优缺点?不要只给一个"标准答案",要提供决策依据,让读者知道在什么场景下该拔哪把刀。

技术写作本质上是对大脑碎片知识的一次"磁盘碎片整理"。当你试图把一个 Bug 的前因后果用大白话给别人讲清楚时,这个知识点才真正长在了你的脑子里。

所以,下次再遇到这种让你拍大腿的"小坑",别急着关掉编辑器。泡杯咖啡,把它写下来。毕竟,踩过的坑如果不变成文字,那就只是白踩了。

相关推荐
字节跳动开源15 分钟前
VisActor全新图可视化开源项目:VGraph
前端·设计模式·开源
打呵欠的猫17 分钟前
前端接口超时从 15s 改到动态值后,用户投诉降了 60%——AI 帮我分析出的方案
前端·ai编程
岁月宁静17 分钟前
二、《从零手撸 Agent》 — 聊聊 LLM 的失忆真相
前端·python·agent
郭邯24 分钟前
用纯前端实现一个代码压缩美化工具,我踩了这三个坑
前端
用户2832096793727 分钟前
从进程线程到 async/await:JS Event-loop事件循环机制底层解析
前端·javascript
岁月宁静36 分钟前
一、《从零手撸 Agent》 我用 10 行代码跑通了第一次大模型调用(顺便踩了 4 个坑)
前端·python·agent
计算机魔术师1 小时前
Fable 5.1 来了,智能体任务成本降 45%——Anthropic 这次为何降价这么狠
前端
Csvn1 小时前
现代 CSS 新特性深度:container query、:has()、@scope 与 subgrid
前端
计算机魔术师1 小时前
Anthropic 自己承认:模型越来越难管住了
前端