前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 热点
旧版
首页程序员面试题库forEach map 修改原数组行为
JavaScript数组与集合

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

forEach 和 map 在开始时只记住了数组长度,没有把元素拍成快照。所以追加到末尾的元素不会被访问,但还没轮到的位置被改值或删除,后面的回调都会看到。

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

固定的是长度,不是内容

  1. 初始长度开始时读一次 length,决定最多访问到哪个索引
  2. 访问时读值每轮先检查索引是否存在,再读取此刻的值
  3. 已访问的索引改掉已经访问过的位置,回调不会重跑

在原长度范围内把一个空位补上值,这个位置之后是会被访问的。所以不能笼统地说遍历中新增的元素都看不到,要看新增在什么位置。

核心回答

先记住这个答案

forEach 和 map 这类方法开始时读一次 length,之后最多只访问到这个长度为止,所以回调里 push 进去的新元素本轮不会被处理。但它们并没有复制数组,每访问一个索引,都会先检查这个位置还在不在,再读取当时的值。所以还没轮到的位置被改了值,后面的回调读到的是新值;被 delete 掉的位置会直接跳过。工程上最好别在遍历里改原数组,要改就先复制一份,或者改完再遍历。

  • 开始时记下长度,末尾追加的元素本轮不访问
  • 没访问到的位置,读到的是当时的最新值
  • 访问前被 delete 的位置会被跳过
  • 回调里 splice 删除当前项会漏掉下一项

在第一次回调里同时追加、删除、改值

下面这段把三种修改都塞进处理 A 的那次回调里,看看后面的回调分别看到了什么。

一次回调里的三种修改JavaScript
const values = ['A', 'B', 'C']
const seen = []

values.forEach((value, index) => {
  seen.push(index + ':' + value)
  if (index === 0) {
    values.push('D')    // 追加到末尾
    delete values[1]    // 删掉还没访问的 B
    values[2] = 'C*'    // 改掉还没访问的 C
  }
})

console.log(JSON.stringify(seen))
console.log(values.length, values[3], Object.hasOwn(values, 1))
查看输出与解释
["0:A","2:C*"]
4 D false

一共只访问了两次。索引 1 在轮到它之前被删了,forEach 检查发现这个位置不存在,就跳过。索引 2 读到的是改过的 C*,说明值是访问时才读的。D 确实加进去了,数组长度变成 4,但开始时记下的长度是 3,所以索引 3 不在这次遍历的范围里。 可以把它想成一个按工位编号点名的班长:开始时数了一下有 3 个工位,就只点 3 个号。点名途中来了新人坐到第 4 个工位,不会被点到;还没点到的工位换了人,点到时看到的就是新人;工位空了,就跳过。

splice 删除当前项为什么会漏掉下一项

比 delete 更常见的坑是在回调里用 splice 删元素。比如要删掉所有无效项,写成 list.forEach((item, i) => { if (!item.valid) list.splice(i, 1) })。

splice 会把后面的元素整体往前挪一位,而 forEach 的索引照常加 1。这时刚挪到当前位置的那个元素就被跳过了。如果两个无效项挨在一起,第二个就会漏删。delete 不挪位置,只留一个空位,所以不会有这个问题,但会留下稀疏数组,也不是好办法。

三种安全的改法
最省事的是不改原数组,直接用 filter 生成新数组:list = list.filter(item => item.valid)。如果必须原地删,就倒着遍历,从最后一个索引往前 splice,前面的元素位置不受影响。第三种是先收集要删除的索引,遍历结束后再统一处理。 另外,复制数组只复制了外层。元素是对象时,[...list] 里的对象和原数组是同一批,在回调里改对象字段,原数组照样能看到。
回答前,多想一步

容易答错的地方

以为遍历开始时整个数组被冻结了
固定的只有长度。没访问到的位置,改值和删除都会被后面的回调看到。需要一份稳定的输入,要自己先复制,比如 [...values].forEach(...)。
以为遍历中新增的元素一律看不到
追加到原长度之外确实看不到。但如果原数组在索引 1 有个空位,处理索引 0 时给它补上值,轮到索引 1 时这个位置已经存在,回调就会执行。判断时要看新增发生在哪个位置、是不是已经访问过。
试着用自己的话回答

面试官还会怎么问?

map 的结果长度会因为回调里的 push 变长吗?

不会。map 在开始时就按原长度创建了结果数组,追加的元素不会被映射,结果长度还是原来的长度。被跳过的空位在结果里也还是空位。

for 循环里修改数组和 forEach 有什么不同?

普通 for 循环的条件 i < arr.length 每轮都会重新读 length,所以循环里 push 进去的元素会被访问到,写得不好还会变成死循环。forEach 只在开始时读一次长度,不会有这个问题。

回调里修改已经访问过的元素会怎样?

值会被改掉,但回调不会为这个位置重新执行。如果后面的逻辑依赖前面处理过的结果,这类修改很容易让数据和处理记录对不上。

从一道题,走向一组知识

把知识连起来

数组与集合

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

Set 没有固定长度这一说,遍历中新增的成员会被访问到,可以和数组的规则对照着记。

数组与集合

map、filter、sort 链式调用会创建几个数组?怎么减少中间分配?

把修改挪到遍历之外的常见做法是生成新数组,这篇讲这样做多出来的内存开销。

参考资料

  • MDN:Array 迭代方法
  • MDN:forEach
  • MDN:map

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

本题目录
  1. 先记住这个答案
  2. 在第一次回调里同时追加、删除、改值
  3. splice 删除当前项为什么会漏掉下一项
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

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

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