前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 热点
旧版
首页程序员面试题库for 循环数组边界 off-by-one
JavaScript数组与集合

遍历数组为什么写 i < length?怎么避免多循环一次或少循环一次?

长度为 n 的数组,下标是 0 到 n - 1。从 0 开始、条件写 i < arr.length,正好循环 n 次,每个下标访问一次。写成 i <= arr.length 会多读一个 undefined,写成 i < arr.length - 1 会漏掉最后一项。

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

写对循环边界要确定三件事

  1. 起点正向从 0 开始,倒序从 length - 1 开始
  2. 终点条件正向 i < length,倒序 i >= 0,都表示「还在范围内」
  3. 循环里访问了谁用到 arr[i + 1] 时,终点要往前收一格

空数组、只有一个元素、最后一项有特殊值,这三种测试数据最容易把边界错误暴露出来。

核心回答

先记住这个答案

数组长度为 n 时,合法下标是 0 到 n - 1,写成区间就是左闭右开的 [0, n)。所以 for (let i = 0; i < arr.length; i++) 从 0 开始、到 n 之前停下,正好 n 次。写成 i <= arr.length 会多访问一次 arr[n],拿到 undefined;写成 i < arr.length - 1 会少一次,漏掉最后一项。i <= arr.length - 1 和 i < arr.length 是等价的,不算错,只是多绕了一步。真正要记住的是:起点、终点和循环体里访问的下标要对得上。

  • 下标范围是 [0, length),左闭右开
  • i <= length 多一次,读到 undefined
  • 比较相邻两项时,i < length - 1 才是对的
  • 边遍历边删除,要倒着遍历或者改用 filter

多一次和少一次各是什么结果

偏一错误(off-by-one)指循环次数刚好多了一次或少了一次,是最常见的边界 bug。用求和看最直观。

三种终点条件求和对比JavaScript
const arr = [10, 20, 30]

let a = 0
for (let i = 0; i <= arr.length; i++) a += arr[i] // 多读一次 arr[3]
console.log('i <= length', a)

let b = 0
for (let i = 0; i < arr.length - 1; i++) b += arr[i] // 少了最后一项
console.log('i < length - 1', b)

let c = 0
for (let i = 0; i < arr.length; i++) c += arr[i]
console.log('i < length', c)
查看输出与解释
i <= length NaN
i < length - 1 30
i < length 60

多一次的情况,arr[3] 是 undefined,于是 60 + undefined 得到 NaN,整个结果都废了。如果循环体里是 arr[i].name,就会直接抛 TypeError。少一次的情况更隐蔽,不报错,只是结果不对,要靠测试数据里最后一项有特殊值才能发现。 i < length 这种写法之所以成为习惯,是因为 length 本身就是个数,「从 0 数 n 个」写出来就是 i < n。像 slice(start, end)、substring 也都是左闭右开,风格统一,记一种就够了。

什么时候 length - 1 反而是对的

终点条件要看循环体访问了什么。循环体里用到 arr[i + 1] 时,最后一个 i 必须是 length - 2,否则 arr[i + 1] 就越界了。

比较相邻两项JavaScript
const nums = [1, 3, 2, 5]
for (let i = 0; i < nums.length - 1; i++) {
  // 最后一项没有下一项,所以终点往前收一格
  if (nums[i] > nums[i + 1]) console.log('逆序', nums[i], nums[i + 1])
}
查看输出与解释
逆序 3 2

这里如果写 i < nums.length,最后一轮会比较 5 > undefined,结果是 false,碰巧不报错,但换成别的逻辑就会出问题。空数组时 length - 1 是 -1,而 0 < -1 不成立,循环一次都不跑,也是安全的。

倒序遍历的边界
倒序时起点是 length - 1,条件是 i >= 0。常见错误是写成 i > 0,漏掉下标 0 的第一项。

边遍历边删除为什么会漏项

条件 i < arr.length 每轮都会重新读 length。如果循环里删了元素,后面的元素整体前移,下标就和元素对不上了。

正向 splice 跳过了相邻的重复项JavaScript
const list = [1, 2, 2, 3]
for (let i = 0; i < list.length; i++) {
  if (list[i] === 2) list.splice(i, 1) // 删掉后,下一个 2 移到了当前位置
}
console.log(JSON.stringify(list))
查看输出与解释
[1,2,3]

i 为 1 时删掉第一个 2,第二个 2 移到了下标 1,但下一轮 i 已经是 2,直接跳过了它。修法是倒着遍历,从 length - 1 往 0 走,删掉的元素只影响已经处理过的位置;或者干脆用 list.filter((x) => x !== 2) 生成新数组。

回答前,多想一步

容易答错的地方

说 i <= length - 1 是错的
它和 i < length 完全等价,只要长度不变、下标是整数就没问题。面试官想听的不是哪种写法「标准」,而是你能说清边界为什么对。
说把 length 缓存到变量里一定更快
现代引擎对 arr.length 的读取已经优化得很好,缓存的性能收益基本可以忽略。反而如果循环里会增删元素,缓存的长度就过期了,可能导致越界或漏项。
试着用自己的话回答

面试官还会怎么问?

二分查找里 left <= right 和 left < right 怎么选?

取决于你把搜索范围定义成闭区间还是左闭右开。right = length - 1 表示闭区间 [left, right],条件用 left <= right;如果 right = length,表示 [left, right),条件用 left < right,缩小时写 right = mid。区间定义和条件一致就不会错。

有没有不用管下标边界的写法?

for...of 直接遍历值,forEach、map 由数组自己控制次数。需要下标时可以写 for (const [i, v] of arr.entries())。只有需要跳着走、倒着走或者提前修改下标时,才需要手写 for 循环。

循环里 push 新元素会怎样?

条件每轮都重新读 length,新 push 的元素也会被遍历到。如果每轮都 push,循环就永远不会结束。forEach 不一样,它在开始时就定好了遍历范围,中途 push 的元素不会被访问。

从一道题,走向一组知识

把知识连起来

数组与集合

为什么空数组调用 every 返回 true,some 却返回 false?

空数组时循环一次都不执行,every 返回 true 正是这个道理。

数组与集合

数组的 keys、values、entries 返回什么?为什么第二次遍历就没数据了?

用 for...of 配合 entries() 同时拿下标和值,就不用自己写边界了。

参考资料

  • MDN:循环与迭代
  • MDN:Array

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

本题目录
  1. 先记住这个答案
  2. 多一次和少一次各是什么结果
  3. 什么时候 length - 1 反而是对的
  4. 边遍历边删除为什么会漏项
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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