前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库Set 迭代中修改元素行为
JavaScript数组与集合

Set 遍历时新增、删除和重新加入成员,会不会重复处理?

Set 按插入顺序遍历,而且遍历过程中是「活」的:新加入的成员会被访问到,还没轮到就被删除的会跳过。一个已经访问过的值,删掉再加回来,会排到末尾被再访问一次。所以 Set 虽然没有重复成员,一轮遍历里同一个值却可能处理两次。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#数组与集合
先看核心答案读代码示例
理解线索

遍历中修改 `Set` 的三种情况

  1. 重复 add 已有值什么都不变,位置也不会移到末尾
  2. 删除还没访问的成员轮到它时已经不在了,直接跳过
  3. 删除后重新加入变成末尾的新成员,本轮还会再访问一次

和数组不同,Set 的遍历没有「开始时记下长度」这一说。回调里每次都删了再加,遍历就永远结束不了。

核心回答

先记住这个答案

Set 按成员第一次成功加入的顺序遍历,对已有的值再 add 不会改变它的位置。遍历过程中修改 Set,规则是这样的:新加入的成员排在末尾,本轮遍历会访问到;还没访问到就被删掉的成员会被跳过;已经访问过的值,删除后重新加入,会作为末尾的新成员再被访问一次。所以 Set 的「不重复」只是说任一时刻集合里没有重复值,不代表一轮遍历中每个值只处理一次。只想处理遍历开始时的成员,就先 [...set] 复制一份快照再遍历。

  • Set 按成员第一次加入的顺序遍历
  • 遍历中新加的成员,本轮会被访问到
  • 删掉再加回来的值会排到末尾,再访问一次
  • 只想处理当前成员,先 [...set] 复制一份

删掉再加回来,同一个值被访问两次

先看重复 add 和删除重插的区别。

一轮遍历访问了两次 AJavaScript
const values = new Set(['A', 'B'])
values.add('A')                          // 已存在,什么都不变
console.log(JSON.stringify([...values]))

const seen = []
let requeued = false
values.forEach(value => {
  seen.push(value)
  if (value === 'A' && !requeued) {
    requeued = true                      // 只重插一次,否则会一直循环
    values.delete('A')
    values.add('A')                      // A 变成末尾的新成员
  }
})
console.log(JSON.stringify(seen))
console.log(JSON.stringify([...values]))
查看输出与解释
["A","B"]
["A","B","A"]
["B","A"]

第一行说明重复 add('A') 没有把 A 挪到后面。遍历时,第一次访问 A 就把它删掉再加回来,A 排到了 B 后面,所以遍历访问完 B 之后又访问了一次 A。最后集合里只有 B 和 A 两个成员,没有重复,但 seen 里 A 出现了两次。 示例用 requeued 标志只重插一次。如果去掉它,每次访问 A 都会把 A 重新排到末尾,遍历就停不下来了,虽然集合始终只有两个成员。

为什么 Set 的遍历是「活」的

可以把 Set 的遍历想象成一个人沿着一条排队的队伍往后走,挨个打招呼。走的过程中,队尾新来的人他会走到;前面还没走到就离队的人,他就碰不到了;已经打过招呼的人离队后又去队尾重新排,他走到队尾时还会再打一次招呼。

和数组 forEach 的对比
数组的 forEach 在开始时记下了长度,末尾追加的元素本轮看不到。Set 没有这个限制,遍历器每次都去找当前位置之后的下一个成员,只要遍历还没结束,新加的就能被找到。另外,Set 删除当前成员不会导致后面的成员被跳过,这点比数组 splice 安全。 for...of 遍历 Set 和 forEach 遵循同样的规则。
回答前,多想一步

容易答错的地方

以为 Set 没有重复值,遍历就不会重复处理
唯一性说的是集合当前的状态。删除再加入会产生一个新位置,已经处理过的值可以再被访问。需要保证每个值只处理一次,要另外用一个集合记录「已处理」。
以为 size 很小,遍历就一定能结束
示例里 size 始终是 2,但只要回调每次都删了再加,遍历就会无限进行下去。判断会不会结束,要看遍历过程中是不是一直在产生新位置,而不是看成员数量。
试着用自己的话回答

面试官还会怎么问?

遍历时想删除符合条件的成员,安全吗?

安全。在 forEach 或 for...of 里直接 delete 当前成员,不会影响后面成员的访问,这是 Set 和 Map 的遍历器专门保证的。真正要小心的是遍历中新增成员。

怎样只处理遍历开始时的那批成员?

先复制一份快照:for (const item of [...set])。之后怎么增删原集合都不影响这次遍历。但要想清楚,快照里的成员如果在遍历中被删了,这次还要不要处理。

用 Set 做任务队列有什么问题?

回调里会不断加入新任务时,Set 的遍历确实能处理到它们,看起来像一个队列。但失败重试、去重、总数上限这些都没法表达,也很难看出什么时候结束。真要做任务队列,用数组加游标,或者专门的队列结构,把规则写清楚。

从一道题,走向一组知识

把知识连起来

数组与集合

forEach 或 map 遍历时修改原数组,哪些变化会被看到?

数组的 forEach 开始时就定下了访问范围,末尾追加的元素看不到,和 Set 正好相反。

数组与集合

Map 的遍历顺序是怎么定的,和对象的属性顺序有什么区别?

Map 的遍历规则和 Set 一样,那篇讲更新已有键和删除重插对顺序的影响。

参考资料

  • MDN:Set.forEach
  • MDN:Set

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 删掉再加回来,同一个值被访问两次
  3. 为什么 Set 的遍历是「活」的
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑