Skip to content

数组迭代方法:声明式处理数据的艺术

回想一下你在餐厅点餐的经历。你不会告诉厨师"先把锅加热,然后倒油,接着放入洋葱...",而是直接说"我要一份意大利面"。厨师知道如何制作,你只需要描述你想要什么。这种方式被称为"声明式"——你声明目标,而不是描述过程。JavaScript 的数组迭代方法采用同样的哲学。你不需要写循环来描述"如何"遍历数组,而是声明你想"做什么"——转换、过滤、查找或归纳数据。这些方法让代码更简洁、更易读、更具表达力。

什么是数组迭代方法

数组迭代方法是用于遍历数组元素并对每个元素执行操作的方法。它们接受一个回调函数作为参数,这个函数会被应用到数组的每个元素上。这些方法是函数式编程的核心工具,让我们能够以声明式的方式处理数据。

让我们先看一个对比,理解为什么迭代方法如此重要:

javascript
let numbers = [1, 2, 3, 4, 5];

// 命令式:告诉计算机"如何做"
let doubled1 = [];
for (let i = 0; i < numbers.length; i++) {
  doubled1.push(numbers[i] * 2);
}

// 声明式:告诉计算机"做什么"
let doubled2 = numbers.map((n) => n * 2);

console.log(doubled1); // [2, 4, 6, 8, 10]
console.log(doubled2); // [2, 4, 6, 8, 10]

声明式代码更简洁、更易读,意图更明确。这就是迭代方法的力量。

forEach() - 遍历数组

forEach() 是最基础的迭代方法,它对数组的每个元素执行一次提供的函数。

基本用法

javascript
let fruits = ["apple", "banana", "orange"];

// 遍历并打印每个元素
fruits.forEach((fruit) => {
  console.log(fruit);
});
// apple
// banana
// orange

// 回调函数可以接收三个参数:元素、索引、数组本身
fruits.forEach((fruit, index, array) => {
  console.log(`${index}: ${fruit} (total: ${array.length})`);
});
// 0: apple (total: 3)
// 1: banana (total: 3)
// 2: orange (total: 3)

与传统 for 循环的对比

javascript
let numbers = [1, 2, 3, 4, 5];

// 传统 for 循环
for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i] * 2);
}

// forEach
numbers.forEach((n) => console.log(n * 2));

// for...of(ES6)
for (let n of numbers) {
  console.log(n * 2);
}

forEach() 的特点:

  • 不能使用 breakcontinue
  • 总是返回 undefined(不返回新数组)
  • 不修改原数组(除非在回调中显式修改)

实际应用

javascript
// 更新对象数组
let products = [
  { name: "Laptop", price: 1000 },
  { name: "Mouse", price: 25 },
  { name: "Keyboard", price: 75 },
];

products.forEach((product) => {
  product.tax = product.price * 0.1;
  product.total = product.price + product.tax;
});

console.log(products);
// [
//   { name: "Laptop", price: 1000, tax: 100, total: 1100 },
//   { name: "Mouse", price: 25, tax: 2.5, total: 27.5 },
//   { name: "Keyboard", price: 75, tax: 7.5, total: 82.5 }
// ]

map() - 映射转换

map() 方法创建一个新数组,包含对原数组每个元素调用函数的结果。这是数据转换的首选方法。

基本用法

javascript
let numbers = [1, 2, 3, 4, 5];

// 将每个数字乘以2
let doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5] - 原数组未变

// 提取对象属性
let users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 30 },
  { name: "Charlie", age: 35 },
];

let names = users.map((user) => user.name);
console.log(names); // ["Alice", "Bob", "Charlie"]

let ages = users.map((user) => user.age);
console.log(ages); // [25, 30, 35]

创建新对象

javascript
let products = [
  { id: 1, name: "Laptop", price: 1000 },
  { id: 2, name: "Mouse", price: 25 },
  { id: 3, name: "Keyboard", price: 75 },
];

// 添加折扣价格
let discounted = products.map((product) => ({
  ...product,
  discount: product.price * 0.2,
  finalPrice: product.price * 0.8,
}));

console.log(discounted[0]);
// {
//   id: 1,
//   name: "Laptop",
//   price: 1000,
//   discount: 200,
//   finalPrice: 800
// }

使用索引参数

javascript
let words = ["apple", "banana", "orange"];

// 添加序号
let numbered = words.map((word, index) => `${index + 1}. ${word}`);
console.log(numbered); // ["1. apple", "2. banana", "3. orange"]

// 根据索引计算
let indexed = [10, 20, 30, 40];
let result = indexed.map((value, index) => value * index);
console.log(result); // [0, 20, 60, 120]

实际应用:数据格式化

javascript
// API 响应数据转换
let apiResponse = [
  {
    user_id: 1,
    user_name: "Alice Johnson",
    email_address: "[email protected]",
    created_at: "2024-01-15",
  },
  {
    user_id: 2,
    user_name: "Bob Smith",
    email_address: "[email protected]",
    created_at: "2024-02-20",
  },
];

// 转换为前端需要的格式
let formattedUsers = apiResponse.map((user) => ({
  id: user.user_id,
  name: user.user_name,
  email: user.email_address,
  joinedDate: new Date(user.created_at).toLocaleDateString(),
}));

console.log(formattedUsers);
// [
//   { id: 1, name: "Alice Johnson", email: "[email protected]", joinedDate: "1/15/2024" },
//   { id: 2, name: "Bob Smith", email: "[email protected]", joinedDate: "2/20/2024" }
// ]

filter() - 过滤元素

filter() 方法创建一个新数组,包含通过测试函数的所有元素。

基本用法

javascript
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 筛选偶数
let evens = numbers.filter((n) => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]

// 筛选大于5的数字
let greaterThanFive = numbers.filter((n) => n > 5);
console.log(greaterThanFive); // [6, 7, 8, 9, 10]

// 筛选符合条件的对象
let users = [
  { name: "Alice", age: 25, active: true },
  { name: "Bob", age: 17, active: true },
  { name: "Charlie", age: 30, active: false },
  { name: "David", age: 22, active: true },
];

let activeAdults = users.filter((user) => user.age >= 18 && user.active);
console.log(activeAdults);
// [
//   { name: "Alice", age: 25, active: true },
//   { name: "David", age: 22, active: true }
// ]

复杂过滤条件

javascript
let products = [
  { name: "Laptop", price: 1200, category: "Electronics", inStock: true },
  { name: "Phone", price: 800, category: "Electronics", inStock: false },
  { name: "Desk", price: 300, category: "Furniture", inStock: true },
  { name: "Chair", price: 150, category: "Furniture", inStock: true },
  { name: "Monitor", price: 400, category: "Electronics", inStock: true },
];

// 多个条件
let affordableElectronics = products.filter(
  (p) => p.category === "Electronics" && p.price < 1000 && p.inStock
);

console.log(affordableElectronics);
// [{ name: "Monitor", price: 400, category: "Electronics", inStock: true }]

移除特定元素

javascript
let tasks = [
  "Buy groceries",
  "Clean house",
  "Exercise",
  "Read book",
  "Exercise",
];

// 移除 "Exercise"
let withoutExercise = tasks.filter((task) => task !== "Exercise");
console.log(withoutExercise); // ["Buy groceries", "Clean house", "Read book"]

// 移除重复值(配合 indexOf)
let unique = tasks.filter(
  (task, index, array) => array.indexOf(task) === index
);
console.log(unique); // ["Buy groceries", "Clean house", "Exercise", "Read book"]

reduce() - 归纳累加

reduce() 是最强大也最复杂的迭代方法。它将数组归纳为单个值,可以实现求和、计数、分组等各种操作。

基本用法:求和

javascript
let numbers = [1, 2, 3, 4, 5];

// 计算总和
let sum = numbers.reduce((accumulator, current) => {
  return accumulator + current;
}, 0); // 0 是初始值

console.log(sum); // 15

// 简化写法
let sum2 = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum2); // 15

reduce() 的工作原理:

  1. 第一次调用:accumulator = 0(初始值),current = 1,返回 0 + 1 = 1
  2. 第二次调用:accumulator = 1current = 2,返回 1 + 2 = 3
  3. 第三次调用:accumulator = 3current = 3,返回 3 + 3 = 6
  4. 第四次调用:accumulator = 6current = 4,返回 6 + 4 = 10
  5. 第五次调用:accumulator = 10current = 5,返回 10 + 5 = 15

计算平均值

javascript
let scores = [85, 90, 78, 92, 88];

let average = scores.reduce((sum, score, index, array) => {
  sum += score;
  // 最后一个元素时计算平均值
  if (index === array.length - 1) {
    return sum / array.length;
  }
  return sum;
}, 0);

console.log(average); // 86.6

查找最大值和最小值

javascript
let numbers = [5, 2, 9, 1, 7, 6];

let max = numbers.reduce((max, current) => (current > max ? current : max));
console.log(max); // 9

let min = numbers.reduce((min, current) => (current < min ? current : min));
console.log(min); // 1

对象数组的统计

javascript
let cart = [
  { product: "Laptop", price: 1000, quantity: 1 },
  { product: "Mouse", price: 25, quantity: 2 },
  { product: "Keyboard", price: 75, quantity: 1 },
];

// 计算总价
let total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
console.log(total); // 1125

// 统计商品数量
let totalItems = cart.reduce((count, item) => count + item.quantity, 0);
console.log(totalItems); // 4

分组和计数

javascript
let votes = ["Alice", "Bob", "Alice", "Charlie", "Bob", "Alice"];

// 计数
let voteCount = votes.reduce((counts, name) => {
  counts[name] = (counts[name] || 0) + 1;
  return counts;
}, {});

console.log(voteCount); // { Alice: 3, Bob: 2, Charlie: 1 }

// 按年龄分组用户
let users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 30 },
  { name: "Charlie", age: 25 },
  { name: "David", age: 30 },
];

let grouped = users.reduce((groups, user) => {
  let age = user.age;
  if (!groups[age]) {
    groups[age] = [];
  }
  groups[age].push(user);
  return groups;
}, {});

console.log(grouped);
// {
//   25: [{ name: "Alice", age: 25 }, { name: "Charlie", age: 25 }],
//   30: [{ name: "Bob", age: 30 }, { name: "David", age: 30 }]
// }

扁平化数组

javascript
let nested = [
  [1, 2],
  [3, 4],
  [5, 6],
];

let flattened = nested.reduce((flat, current) => flat.concat(current), []);
console.log(flattened); // [1, 2, 3, 4, 5, 6]

find() 和 findIndex() - 查找元素

find() - 查找第一个匹配元素

find() 返回数组中第一个满足测试函数的元素,如果没找到返回 undefined

javascript
let users = [
  { id: 1, name: "Alice", role: "admin" },
  { id: 2, name: "Bob", role: "user" },
  { id: 3, name: "Charlie", role: "user" },
];

// 查找第一个管理员
let admin = users.find((user) => user.role === "admin");
console.log(admin); // { id: 1, name: "Alice", role: "admin" }

// 按 ID 查找
let user = users.find((u) => u.id === 2);
console.log(user); // { id: 2, name: "Bob", role: "admin" }

// 找不到返回 undefined
let notFound = users.find((u) => u.id === 999);
console.log(notFound); // undefined

findIndex() - 查找元素索引

findIndex() 返回第一个满足测试函数的元素的索引,如果没找到返回 -1。

javascript
let numbers = [5, 12, 8, 130, 44];

// 查找第一个大于10的数字的索引
let index = numbers.findIndex((n) => n > 10);
console.log(index); // 1 (值为12的索引)

// 查找对象索引
let products = [
  { id: 1, name: "Laptop" },
  { id: 2, name: "Mouse" },
  { id: 3, name: "Keyboard" },
];

let mouseIndex = products.findIndex((p) => p.name === "Mouse");
console.log(mouseIndex); // 1

some() 和 every() - 条件检查

some() - 至少一个满足

some() 检查数组中是否至少有一个元素满足测试函数。

javascript
let numbers = [1, 2, 3, 4, 5];

// 是否有偶数
let hasEven = numbers.some((n) => n % 2 === 0);
console.log(hasEven); // true

// 是否有大于10的数
let hasLarge = numbers.some((n) => n > 10);
console.log(hasLarge); // false

// 实际应用:权限检查
let userPermissions = ["read", "write"];
let requiredPermissions = ["read", "delete"];

let hasAnyPermission = requiredPermissions.some((perm) =>
  userPermissions.includes(perm)
);
console.log(hasAnyPermission); // true (有 read 权限)

every() - 全部都满足

every() 检查数组中是否所有元素都满足测试函数。

javascript
let numbers = [2, 4, 6, 8, 10];

// 是否全是偶数
let allEven = numbers.every((n) => n % 2 === 0);
console.log(allEven); // true

// 是否都大于5
let allGreaterThanFive = numbers.every((n) => n > 5);
console.log(allGreaterThanFive); // false

// 实际应用:表单验证
let formFields = [
  { name: "email", value: "[email protected]", valid: true },
  { name: "password", value: "12345678", valid: true },
  { name: "age", value: "", valid: false },
];

let formIsValid = formFields.every((field) => field.valid);
console.log(formIsValid); // false

flat() 和 flatMap() - 扁平化操作

flat() - 扁平化嵌套数组

flat() 方法创建一个新数组,包含所有子数组元素,可以指定扁平化深度。

javascript
// 默认扁平化一层
let arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); // [1, 2, 3, 4]

// 扁平化多层
let arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); // [1, 2, 3, 4, [5, 6]]
console.log(arr2.flat(2)); // [1, 2, 3, 4, 5, 6]

// 完全扁平化(使用 Infinity)
let arr3 = [1, [2, [3, [4, [5]]]]];
console.log(arr3.flat(Infinity)); // [1, 2, 3, 4, 5]

// 移除空项
let arr4 = [1, 2, , 4, 5];
console.log(arr4.flat()); // [1, 2, 4, 5]

flatMap() - 映射后扁平化

flatMap() 相当于先 map()flat(),但更高效。

javascript
let sentences = ["Hello world", "How are you"];

// 使用 map + flat
let words1 = sentences.map((s) => s.split(" ")).flat();
console.log(words1); // ["Hello", "world", "How", "are", "you"]

// 使用 flatMap
let words2 = sentences.flatMap((s) => s.split(" "));
console.log(words2); // ["Hello", "world", "How", "are", "you"]

// 过滤并扁平化
let numbers = [1, 2, 3, 4];
let result = numbers.flatMap((n) => (n % 2 === 0 ? [n, n * 2] : []));
console.log(result); // [2, 4, 4, 8]

链式调用的力量

迭代方法可以链式调用,创建强大的数据处理管道:

javascript
let products = [
  { name: "Laptop", price: 1200, category: "Electronics", rating: 4.5 },
  { name: "Phone", price: 800, category: "Electronics", rating: 4.8 },
  { name: "Desk", price: 300, category: "Furniture", rating: 4.2 },
  { name: "Chair", price: 150, category: "Furniture", rating: 4.0 },
  { name: "Monitor", price: 400, category: "Electronics", rating: 4.6 },
];

// 复杂的数据处理管道
let avgElectronicsPrice = products
  .filter((p) => p.category === "Electronics") // 筛选电子产品
  .filter((p) => p.rating >= 4.5) // 筛选高评分
  .map((p) => p.price) // 提取价格
  .reduce(
    (
      sum,
      price,
      _,
      array // 计算平均价格
    ) => sum + price / array.length,
    0
  );

console.log(avgElectronicsPrice); // 800

// 另一个例子:用户数据处理
let users = [
  { name: "Alice", age: 28, orders: [100, 200, 50] },
  { name: "Bob", age: 35, orders: [300, 150] },
  { name: "Charlie", age: 22, orders: [75, 125, 175] },
];

let topSpenders = users
  .map((user) => ({
    name: user.name,
    totalSpent: user.orders.reduce((sum, amount) => sum + amount, 0),
  }))
  .filter((user) => user.totalSpent > 200)
  .sort((a, b) => b.totalSpent - a.totalSpent)
  .map((user) => user.name);

console.log(topSpenders); // ["Bob", "Charlie"]

性能考虑

虽然链式调用很优雅,但每个方法都会遍历数组一次。在处理大型数组时,可能需要优化:

javascript
let largeArray = Array.from({ length: 100000 }, (_, i) => i);

// ❌ 多次遍历(3次)
console.time("chained");
let result1 = largeArray
  .filter((n) => n % 2 === 0)
  .map((n) => n * 2)
  .filter((n) => n > 1000);
console.timeEnd("chained");

// ✅ 单次遍历(使用 reduce)
console.time("single");
let result2 = largeArray.reduce((acc, n) => {
  if (n % 2 === 0) {
    let doubled = n * 2;
    if (doubled > 1000) {
      acc.push(doubled);
    }
  }
  return acc;
}, []);
console.timeEnd("single");

// 两者结果相同,但第二种方式更快

总结

数组迭代方法是 JavaScript 函数式编程的核心,它们提供了声明式的数据处理方式:

  • forEach() - 遍历数组,执行 side effect
  • map() - 转换每个元素,返回新数组
  • filter() - 筛选符合条件的元素
  • reduce() - 将数组归纳为单个值,功能最强大
  • find() / findIndex() - 查找匹配元素或索引
  • some() / every() - 检查是否有/全部元素满足条件
  • flat() / flatMap() - 扁平化嵌套数组

使用迭代方法的优势:

  • 代码更简洁、可读性更强
  • 声明式编程,关注"做什么"而非"怎么做"
  • 支持链式调用,构建数据处理管道
  • 函数式编程风格,易于测试和维护

这些方法是现代 JavaScript 开发的基石,掌握它们将大大提升你的编程效率和代码质量。配合之前学习的数组基础和其他数组方法,你现在已经具备了全面的数组操作能力。