先记住这个答案
普通数组长度为 n 时,合法位置通常是零到 n 减一,所以正向扫描常写 for (let i = 0; i < array.length; i++),对应半开区间 [0, n)。写 i <= length 会多读越界位置,写 i < length - 1 会遗漏末项;i <= length - 1 在长度不变且整数索引的前提下也可以正确。真正要避免的是起点、终点包含规则和步进不一致。空数组、单元素和末项有特殊值的测试最容易暴露偏一错误,循环中修改长度则需要另外定义扫描范围。
- 零基索引的尾部边界不包含 length
- 小于 length减一漏末项,小于等于 length 多一项
- 半开区间便于表达空区间与片段拼接
把正常、遗漏和越界放在同一输入上
示例选择最后一项明显不同的数组,正确循环保留全部值,错误的小于 length 减一漏掉末项。越界版本不直接把 undefined 序列化,而是明确标记,避免 JSON 的 null 转换掩盖问题。
测试空数组与单元素尤其有效:空数组不该进入循环,单元素应恰好处理一次。只用多元素且值相同的输入,遗漏或重复处理可能不容易被观察到。
const values = ['first', 'last'];
const correct = [];
for (let i = 0; i < values.length; i++) correct.push(values[i]);
const missed = [];
for (let i = 0; i < values.length - 1; i++) missed.push(values[i]);
const extra = [];
for (let i = 0; i <= values.length; i++) extra.push(values[i] === undefined ? 'out-of-range' : values[i]);
console.log(JSON.stringify(correct));
console.log(JSON.stringify(missed));
console.log(JSON.stringify(extra));查看输出与解释
["first","last"]
["first"]
["first","last","out-of-range"]错误来自比较符号与终点定义不匹配,而不是 length 减一这个表达式本身非法。示例输入不含真实 undefined,才能把最后标记明确归因于额外读取。
子区间同样适合用不包含终点表示
处理索引 start 到 end 之前的项目时,循环条件 i < end 与 slice 的区间直觉容易对齐。相邻片段可以使用前一段终点作为后一段起点,减少重叠或缺口。
边界若来自用户参数,应先校验整数、范围和起止关系,不能依赖 JavaScript 隐式转换来修复。对于允许空区间的接口,start 等于 end 应有明确结果,而不是被误判成必须再处理一项。
长度变化会让看似正确的条件改变意义
每轮读取 array.length 的循环可能看到后来追加的元素;若回调持续追加,就可能无法结束。缓存初始长度只固定扫描上界,不会冻结未读取元素的值,也不会阻止删除导致的缺口。
正向遍历中 splice 删除当前项会让后续元素左移,索引继续增加可能跳过一项。应根据需求选择倒序删除、构造新结果或明确调整索引,不要在一个循环里混用多种未说明的结构修改。
容易答错的地方
- 把 i 小于等于 length减一一概判成错误
- 在长度稳定、索引按整数递增的条件下它与 i 小于 length 等价。代码评审应检查真实边界契约,而不是机械禁止某个表达式;半开写法的优势主要是更容易组合和理解。
- 把缓存 length 当成完整快照
- 它只固定上界,数组元素仍可能被修改或删除,索引存在性也会变化。需要处理固定批次时应明确复制或版本策略,不能用一个局部数字代替数据一致性设计。
面试官还会怎么问?
倒序遍历的终止条件怎么写?
常从 length 减一开始,在 i 大于等于零时继续并每轮递减。空数组起点为负一,循环自然不执行;若要倒序处理子区间,应重新写清包含起点和终点规则再推导。
为什么多读一次容易得到 NaN?
额外位置常读到 undefined,参与普通数值运算后可能传播为 NaN。应首先检查合法索引和输入类型,而不是在最终结果处把 NaN 强行替换成零,避免掩盖真正的循环错误。
什么时候改成 for...of 更清楚?
只关心值且不需要复杂索引边界时,for...of 可以减少手写索引错误。若处理相邻位置、指定子区间或原地修改,显式索引仍有价值,选择应围绕算法实际需要的信息。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。