先记住这个答案
forEach、map 等方法开始时读取长度作为扫描上界,但访问每个索引时仍检查属性是否存在并读取当时的值。因此追加到原长度之外的元素通常不会被本轮处理,未访问位置改值会影响后续回调,访问前已删除且无继承同名属性的位置会被跳过。原范围内原本空缺的位置若在访问前补上,也可能被访问;不能说所有新增元素都被忽略。这些是可推导的语义,不应笼统归因于引擎随机差异。复杂业务最好把扫描与结构变更分开,明确固定批次还是动态工作队列。
- 初始长度上界不是完整内容快照
- 未访问索引读取的是当时存在的属性和值
- 新增是否可见取决于位置与发生时机
把追加、删除和改值放在首次回调里
示例开始长度三,处理 A 时追加 D、删除索引一并更新索引二。后续跳过已删除的一,读取改写后的 C,而新增 D 超出初始上界,不会被此次 forEach 访问。
输出同时保留访问索引和最终长度,避免只看元素文本误以为 D 没有被成功添加。是否写入数组与是否属于当前扫描范围,是两件可以同时成立而结果不同的事。
const values = ['A', 'B', 'C'];
const seen = [];
values.forEach((value, index) => {
seen.push(index + ':' + value);
if (index === 0) {
values.push('D');
delete values[1];
values[2] = 'C*';
}
});
console.log(JSON.stringify(seen));
console.log(values.length, values[3], Object.hasOwn(values, 1));查看输出与解释
["0:A","2:C*"]
4 D falseD 已存在于数组中但不在本轮原始扫描范围,一号位置在访问前被删除,二号位置读取新值。结果可以按固定上界与逐索引属性读取推导,不需要诉诸不确定执行顺序。
原范围内补入空位与尾部追加不同
如果原数组长度三且一号位置为空,在处理零号时补入一号,它仍位于尚未访问的原范围内,因此之后可以被处理。候选题中“新增一律跳过”的概括遗漏了位置这一关键条件。
相反,修改已经访问的零号不会让回调重新开始。业务如果希望新增工作立即被处理,应使用明确队列和终止条件,不能把数组回调方法当成隐式动态任务调度器。
删除导致前移时要检查是否跳项
splice 删除当前项会让后续元素向前移动,而扫描索引仍继续增加,于是刚移到当前位置的元素可能被跳过。这与 delete 留下空位的行为不同,排查时需要确认具体修改方法。
可选择先生成删除计划、用 filter 构造新数组或针对索引设计倒序操作。复制数组也只固定外层引用序列,对象内部仍可能被修改;是否需要更深快照取决于正在保护的业务一致性。
容易答错的地方
- 认为保存初始长度就保存了全部值
- 回调读取未访问位置时仍能看到改值或删除,因此整个数组并未被冻结。需要稳定输入批次时应明确复制与版本边界,不能用长度快照推导数据值不变。
- 把所有新增元素都归为不会访问
- 尾部追加到原范围外与在原范围内补上缺失索引不同。应结合索引是否已访问、是否在原上界内以及访问时属性是否存在逐步判断,避免背一个过度简化的结论。
面试官还会怎么问?
map 的结果长度会跟着追加增长吗?
普通 map 按开始时读取的长度创建对应结果范围,原数组后来追加的尾部项不会自动扩展本轮映射结果。未被访问的缺失索引还可能保留结果空位,应按具体属性变化检查。
修改对象元素字段能被后面看到吗?
数组保存对象引用,其他位置或外部代码若持有同一对象,就可能看到字段变化。固定数组长度或浅复制并不隔离对象内部状态,副作用分析需要继续追踪引用身份。
能把遍历中修改说成未定义行为吗?
不能一概这样说,这些方法对长度、属性检查和访问时机有明确规则。复杂修改容易难读,工程上可以避免,但应区分不推荐的写法与语言没有定义的行为。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。