Ajiu9

JavaScript 循环方法详解:for...of、for...in、map、forEach 的正确使用

Aug 21, 2025 · 12min

问题背景

在 JavaScript 开发中,遍历数据是最常见的操作之一。for...offor...inmapforEach 这四种方法看似都能实现循环遍历,但它们的设计目的、适用场景和底层机制都有重要差异。

很多开发者(尤其是初学者)容易混淆这些方法,导致在项目中使用了不合适的循环方式,不仅影响代码可读性,还可能引入难以发现的 bug。

本文将从以下三个方面详细讲解:

  1. 核心差异:每种方法的设计目的和机制
  2. 使用场景:什么情况下用哪种方法
  3. 常见陷阱:实际开发中容易踩的坑

核心概念对比

1. for…in - 遍历对象的可枚举属性

for...in 语句以任意顺序迭代一个对象的除 Symbol 以外的可枚举属性。

基本用法

const obj = { name: '小明', age: 18 }

for (const key in obj)
  console.log(key) // 输出:name, age
console.log(obj[key]) // 输出:小明, 18

重要特性

1. 遍历原型链属性

for...in 会遍历对象自身以及继承的可枚举属性:

const parent = { type: 'person' }
const child = Object.create(parent)
child.name = '小明'

for (const key in child)
  console.log(key) // 输出:name, type

// 只遍历自身属性
for (const key in child) {
  if (Object.hasOwn(child, key))
    console.log(key)
} // 输出:name

2. 数组使用需谨慎

虽然 for...in 可以遍历数组,但不推荐使用:

const arr = ['a', 'b', 'c']

for (const key in arr)
  console.log(key) // 输出:0, 1, 2(字符串类型的索引)

// 问题:可能会遍历到非索引属性
arr.customProp = '自定义属性'
for (const key in arr)
  console.log(key) // 输出:0, 1, 2, customProp

适用场景

  • ✅ 遍历普通对象的属性
  • ✅ 需要检查对象及其原型链上的属性
  • ❌ 遍历数组(应该用 for...of 或数组方法)

2. for…of - 遍历可迭代对象

for...of 语句在可迭代对象(包括 Array、Map、Set、String、TypedArray、arguments 对象等)上创建一个迭代循环。

基本用法

const arr = ['🍎', '🍐', '🍊']

for (const fruit of arr)
  console.log(fruit) // 输出:🍎, 🍐, 🍊

// 遍历字符串
for (const char of 'Hello')
  console.log(char) // 输出:H, e, l, l, o

// 遍历 Map
const map = new Map([['a', 1], ['b', 2]])
for (const [key, value] of map)
  console.log(key, value) // 输出:a 1, b 2

重要特性

1. 只遍历可迭代对象

for...of 只能用于实现了 [Symbol.iterator] 方法的对象:

const obj = { name: '小明' }

// TypeError: obj is not iterable
for (const item of obj)
  console.log(item)

// 数组、字符串、Map、Set、NodeList 等都是可迭代的
const elements = document.querySelectorAll('div')
for (const el of elements)
  console.log(el) // 可以正常遍历

2. 支持 break 和 continue

const arr = [1, 2, 3, 4, 5]

// 找到第一个大于 3 的数字就停止
for (const num of arr) {
  if (num > 3) {
    console.log(num) // 输出:4
    break
  }
}

// 跳过偶数
for (const num of arr) {
  if (num % 2 === 0) continue
  console.log(num) // 输出:1, 3, 5
}

适用场景

  • ✅ 遍历数组(推荐)
  • ✅ 遍历字符串、Map、Set 等
  • ✅ 需要中途退出循环的场景
  • ❌ 遍历普通对象(除非转换为可迭代对象)

3. forEach - 数组的专用遍历方法

forEach() 方法对数组的每个元素执行一次给定的函数。

基本用法

const arr = [1, 2, 3]

arr.forEach((item, index, array) => {
  console.log(`索引 ${index} 的值是 ${item}`)
  // 输出:
  // 索引 0 的值是 1
  // 索引 1 的值是 2
  // 索引 2 的值是 3
})

重要特性

1. 无法中断循环

forEach 没有"停止"机制,即使在回调中使用 breakreturn 也无法中断:

const arr = [1, 2, 3, 4, 5]

// ❌ 错误示例:无法提前退出
arr.forEach((num) => {
  if (num === 3) return // 只能跳过当前这次,不能退出整个循环
  console.log(num) // 输出:1, 2, 4, 5
})
// ❌ 错误示例:break 会抛出语法错误
arr.forEach((num) => {
  if (num === 3) break // SyntaxError: Illegal break statement
  console.log(num)
})

// ✅ 正确做法:如果需要中断,使用 for...of
for (const num of arr) {
  if (num === 3) break
  console.log(num) // 输出:1, 2
}

2. 不改变原数组,但回调函数可以改变

const arr = [1, 2, 3]

// forEach 本身不会改变数组
arr.forEach(num => num * 2)
console.log(arr) // [1, 2, 3]

// 但回调函数可以修改原数组
arr.forEach((num, index) => {
  arr[index] = num * 2
})
console.log(arr) // [2, 4, 6]

适用场景

  • ✅ 不需要中断的数组遍历
  • ✅ 执行副作用操作(如 console.log、修改外部变量)
  • ❌ 需要提前退出的场景
  • ❌ 需要返回新数组的场景(应该用 map

4. map - 数组转换的函数式方法

map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

基本用法

const numbers = [1, 2, 3]

// 每个元素乘以 2
const doubled = numbers.map(num => num * 2)
console.log(doubled) // [2, 4, 6]
console.log(numbers) // [1, 2, 3](原数组不变)

// 转换对象数组
const users = [
  { name: '小明', age: 18 },
  { name: '小红', age: 20 }
]

const names = users.map(user => user.name)
console.log(names) // ['小明', '小红']

重要特性

1. 必须有返回值

如果不返回值,新数组对应位置会是 undefined

const arr = [1, 2, 3]

// ❌ 错误示例:忘记 return
const result = arr.map((num) => {
  num * 2 // 没有 return
})
console.log(result) // [undefined, undefined, undefined]

// ✅ 正确示例:使用箭头函数的隐式返回
const result2 = arr.map(num => num * 2)
console.log(result2) // [2, 4, 6]

// ✅ 正确示例:显式 return
const result3 = arr.map((num) => {
  return num * 2
})
console.log(result3) // [2, 4, 6]

2. 不会改变原数组

const arr = [1, 2, 3]
const doubled = arr.map(num => num * 2)

console.log(arr) // [1, 2, 3]
console.log(doubled) // [2, 4, 6]

3. 可以链式调用

const numbers = [1, 2, 3, 4, 5]

const result = numbers
  .filter(num => num > 2) // 过滤大于 2 的数
  .map(num => num * 2) // 每个数乘以 2
  .reduce((sum, num) => sum + num, 0) // 求和

console.log(result) // 18 (6 + 8 + 10)

适用场景

  • ✅ 数组转换(返回新数组)
  • ✅ 数据格式化
  • ✅ 函数式编程风格
  • ❌ 不需要返回值的遍历(应该用 forEach

核心差异对比表

方法遍历目标返回值能否中断适用场景是否改变原数组
for...in对象的可枚举属性✅ break/continue遍历对象属性否(但回调可以)
for...of可迭代对象的值✅ break/continue遍历数组、字符串等否(但回调可以)
forEach数组undefined执行副作用操作否(但回调可以)
map数组新数组数据转换

实际开发中的选择策略

场景一:遍历普通对象

const user = {
  name: '小明',
  age: 18,
  email: 'xiaoming@example.com'
}

// ✅ 推荐:使用 for...in
for (const key in user)
  console.log(`${key}: ${user[key]}`)

// ✅ 或者转换为数组后使用数组方法
Object.keys(user).forEach((key) => {
  console.log(`${key}: ${user[key]}`)
})

场景二:遍历数组并可能提前退出

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// ✅ 推荐:使用 for...of
for (const num of numbers) {
  if (num > 5) {
    console.log('找到第一个大于 5 的数字:', num)
    break
  }
}

// ❌ 不推荐:forEach 无法中断
numbers.forEach((num) => {
  if (num > 5) {
    // 无法在这里中断整个循环
    console.log(num) // 会继续遍历后续元素
  }
})

场景三:转换数组数据

const products = [
  { name: '苹果', price: 5 },
  { name: '香蕉', price: 3 },
  { name: '橙子', price: 4 }
]

// ✅ 推荐:使用 map
const prices = products.map(p => p.price)
console.log(prices) // [5, 3, 4]

// ❌ 不推荐:forEach 需要手动创建数组
const prices2 = []
products.forEach(p => prices2.push(p.price))
console.log(prices2) // [5, 3, 4]

场景四:执行副作用操作

const users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' }
]

// ✅ 推荐:使用 forEach
users.forEach((user) => {
  console.log(`用户 ID: ${user.id}, 姓名: ${user.name}`)
  // 执行其他副作用操作...
})

// ❌ 不推荐:map 但不使用返回值
users.map((user) => {
  console.log(user)
  return user // 不必要的返回值
})

常见陷阱与最佳实践

陷阱 1:使用 for…in 遍历数组

const arr = [1, 2, 3]

// ❌ 不推荐
for (const index in arr)
  console.log(arr[index]) // 虽然"能用",但有很多问题

// ✅ 推荐
for (const item of arr)
  console.log(item)

问题:

  • 遍历顺序可能不是按数字索引顺序
  • 会遍历到非索引的自定义属性
  • 性能不如 for...of

陷阱 2:在 forEach 中使用 break

const arr = [1, 2, 3, 4, 5]

// ❌ 错误:会抛出语法错误
arr.forEach(num => {
  if (num === 3) break // SyntaxError
  console.log(num)
})

// ✅ 正确:使用 for...of
for (const num of arr) {
  if (num === 3) break
  console.log(num) // 输出:1, 2
}

陷阱 3:map 忘记返回值

const arr = [1, 2, 3]

// ❌ 错误:返回 [undefined, undefined, undefined]
const result = arr.map((num) => {
  num * 2
})

// ✅ 正确
const result2 = arr.map(num => num * 2)
// 或
const result3 = arr.map((num) => {
  return num * 2
})

陷阱 4:修改原数组的误解

很多人误以为 forEachmap 不会修改原数组:

const arr = [1, 2, 3]

// forEach 本身不修改,但回调可以修改
arr.forEach((num, index) => {
  arr[index] = num * 2 // 直接修改原数组
})
console.log(arr) // [2, 4, 6]

// map 不会修改原数组
const arr2 = [1, 2, 3]
const doubled = arr2.map(num => num * 2)
console.log(arr2) // [1, 2, 3]
console.log(doubled) // [2, 4, 6]

最佳实践总结

  1. 遍历对象属性for...in + hasOwnProperty 检查
  2. 遍历数组 → 优先使用 for...of 或数组方法
  3. 需要中断循环 → 必须用 for...of 或传统 for 循环
  4. 数组转换 → 使用 map
  5. 执行副作用 → 使用 forEach(不需要返回值时)
  6. 链式操作 → 使用 mapfilterreduce 等函数式方法

性能对比

对于大规模数组遍历,性能差异如下:

const arr = Array.from({ length: 1000000 }, (_, i) => i)

console.time('for')
for (let i = 0; i < arr.length; i++)
  arr[i] * 2

console.timeEnd('for') // 最快

console.time('for...of')
for (const num of arr)
  num * 2

console.timeEnd('for...of') // 第二快

console.time('forEach')
arr.forEach(num => num * 2)
console.timeEnd('forEach') // 中等

console.time('map')
arr.map(num => num * 2)
console.timeEnd('map') // 最慢(因为要创建新数组)

性能排序for > for...of > forEach > map

::: tip 注意 性能差异在大多数场景下可以忽略,优先考虑代码可读性。只有在处理大规模数据时才需要关注性能优化。 :::

总结

本文详细讲解了 JavaScript 四种循环方法的核心差异:

  • for…in:遍历对象的可枚举属性,包括原型链,适合对象属性遍历
  • for…of:遍历可迭代对象的值,支持中断,是遍历数组的首选
  • forEach:数组专用,无法中断,适合执行副作用操作
  • map:数组转换,返回新数组,适合数据转换和函数式编程

选择合适的循环方法可以让代码更清晰、更高效。记住:工具要用在合适的场景,而不是所有场景都用同一个工具

相关资源

> comment on mastodon / twitter
>
@2024-2025 湘ICP备2024048835号