跳到主要内容

数组方法

JavaScript 数组提供了许多强大的函数式方法,这些方法让数据处理变得简洁而优雅。理解这些方法不仅是掌握函数式编程的基础,也是日常开发中必不可少的技能。

为什么使用数组方法?

传统的 for 循环虽然灵活,但容易出错,而且代码冗长。数组方法提供了一种更声明式的方式来处理数据:

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

// 传统 for 循环:描述"怎么做"
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}

// 数组方法:描述"做什么"
const doubled = numbers.map(x => x * 2);

数组方法的优势:

  • 声明式:描述要做什么,而不是怎么做
  • 不可变:不修改原数组,返回新数组
  • 可组合:可以链式调用
  • 可读性:意图更明确

map - 映射转换

map 是最常用的数组方法之一,它将数组中的每个元素通过一个函数转换为新值,返回一个新数组。

基本用法

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

// 将每个数字乘以 2
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 原数组不变
console.log(numbers); // [1, 2, 3, 4, 5]

map 的执行过程

理解 map 如何工作很重要:

// map 的内部实现(概念上)
function map(array, transform) {
const result = [];
for (let i = 0; i < array.length; i++) {
result.push(transform(array[i], i, array));
}
return result;
}

// 回调函数接收三个参数
const arr = ['a', 'b', 'c'];
arr.map((value, index, array) => {
console.log(`值: ${value}, 索引: ${index}`);
return value.toUpperCase();
});
// 值: a, 索引: 0
// 值: b, 索引: 1
// 值: c, 索引: 2
// ['A', 'B', 'C']

实际应用

// 提取对象数组中的特定属性
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 }
];

// 只获取名字
const names = users.map(user => user.name);
console.log(names); // ['张三', '李四', '王五']

// 转换为另一种结构
const userSummaries = users.map(user => ({
fullName: user.name,
isAdult: user.age >= 18
}));
// [{ fullName: '张三', isAdult: true }, ...]

// 格式化数据
const prices = [100, 200, 300];
const formatted = prices.map(price => `¥${price.toFixed(2)}`);
console.log(formatted); // ['¥100.00', '¥200.00', '¥300.00']

// 解析数据
const jsonStrings = ['{"x":1}', '{"x":2}', '{"x":3}'];
const parsed = jsonStrings.map(str => JSON.parse(str));
// [{ x: 1 }, { x: 2 }, { x: 3 }]

filter - 过滤筛选

filter 根据条件筛选数组元素,返回满足条件的元素组成的新数组。

基本用法

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

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

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

filter 的工作原理

// filter 的内部实现(概念上)
function filter(array, predicate) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i, array)) {
result.push(array[i]);
}
}
return result;
}

// 回调函数返回 true 时保留元素,返回 false 时过滤掉
const arr = [0, 1, false, 2, '', 3, null, undefined];
const truthy = arr.filter(Boolean); // 技巧:过滤假值
console.log(truthy); // [1, 2, 3]

实际应用

const products = [
{ name: 'iPhone', price: 7999, inStock: true },
{ name: 'MacBook', price: 12999, inStock: false },
{ name: 'iPad', price: 4999, inStock: true },
{ name: 'AirPods', price: 1999, inStock: true }
];

// 只保留有库存的商品
const available = products.filter(p => p.inStock);
// iPhone, iPad, AirPods

// 有库存且价格低于 5000
const affordable = products.filter(p => p.inStock && p.price < 5000);
// iPad, AirPods

// 价格区间筛选
const midRange = products.filter(p => p.price >= 3000 && p.price <= 8000);
// iPhone, iPad

// 根据搜索词过滤
const searchTerm = 'i';
const searchResults = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// iPhone, iPad, AirPods

reduce - 归约累积

reduce 是最强大的数组方法,它可以将数组"归约"为单个值。理解 reduce 是成为 JavaScript 高手的关键。

基本用法

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

// 求和
const sum = numbers.reduce((accumulator, current) => {
return accumulator + current;
}, 0); // 0 是初始值

console.log(sum); // 15

reduce 的工作原理

// reduce 的执行过程
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((acc, cur, index, array) => {
console.log(`索引 ${index}: acc=${acc}, cur=${cur}`);
return acc + cur;
}, 0);

// 执行过程:
// 索引 0: acc=0, cur=1 → 返回 1
// 索引 1: acc=1, cur=2 → 返回 3
// 索引 2: acc=3, cur=3 → 返回 6
// 索引 3: acc=6, cur=4 → 返回 10
// 索引 4: acc=10, cur=5 → 返回 15
// 最终结果: 15

常见用途

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

// 求最大值
const max = numbers.reduce((max, cur) => cur > max ? cur : max, -Infinity);
console.log(max); // 5

// 求最小值
const min = numbers.reduce((min, cur) => cur < min ? cur : min, Infinity);
console.log(min); // 1

// 计算平均值
const avg = numbers.reduce((sum, cur, _, arr) => sum + cur, 0) / numbers.length;
console.log(avg); // 3

// 数组扁平化
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, cur) => [...acc, ...cur], []);
console.log(flat); // [1, 2, 3, 4, 5, 6]

// 统计元素出现次数
const chars = ['a', 'b', 'a', 'c', 'b', 'a'];
const count = chars.reduce((acc, char) => {
acc[char] = (acc[char] || 0) + 1;
return acc;
}, {});
console.log(count); // { a: 3, b: 2, c: 1 }

// 数组分组
const people = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 },
{ name: '王五', age: 25 },
{ name: '赵六', age: 30 }
];

const byAge = people.reduce((acc, person) => {
const key = person.age;
if (!acc[key]) acc[key] = [];
acc[key].push(person);
return acc;
}, {});
// { 25: [{张三}, {王五}], 30: [{李四}, {赵六}] }

// 数组转对象
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];

const userMap = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
// { 1: {id:1,name:'张三'}, 2: {id:2,name:'李四'}, 3: {id:3,name:'王五'} }

用 reduce 实现 map 和 filter

// 用 reduce 实现 map
const map = (arr, fn) => arr.reduce((acc, val, i) => {
acc.push(fn(val, i, arr));
return acc;
}, []);

const doubled = map([1, 2, 3], x => x * 2);
console.log(doubled); // [2, 4, 6]

// 用 reduce 实现 filter
const filter = (arr, predicate) => arr.reduce((acc, val, i) => {
if (predicate(val, i, arr)) acc.push(val);
return acc;
}, []);

const evens = filter([1, 2, 3, 4], x => x % 2 === 0);
console.log(evens); // [2, 4]

find 和 findIndex - 查找元素

find - 查找元素

find 返回满足条件的第一个元素,如果没有找到则返回 undefined

const users = [
{ id: 1, name: '张三', role: 'admin' },
{ id: 2, name: '李四', role: 'user' },
{ id: 3, name: '王五', role: 'user' }
];

// 查找 ID 为 2 的用户
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: '李四', role: 'user' }

// 查找管理员
const admin = users.find(u => u.role === 'admin');
console.log(admin); // { id: 1, name: '张三', role: 'admin' }

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

// 使用索引
const indexed = ['a', 'b', 'c', 'b'];
const secondB = indexed.find((val, i) => val === 'b' && i > 1);
console.log(secondB); // 'b'(第二个 b,索引 3)

findIndex - 查找索引

findIndex 返回满足条件的第一个元素的索引,如果没有找到则返回 -1

const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];

// 查找王五的索引
const index = users.findIndex(u => u.name === '王五');
console.log(index); // 2

// 没找到返回 -1
const notFoundIndex = users.findIndex(u => u.id === 999);
console.log(notFoundIndex); // -1

// 配合 splice 删除(但推荐用 filter)
const removeById = (arr, id) => {
const index = arr.findIndex(item => item.id === id);
if (index !== -1) {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
return arr;
};

some 和 every - 条件判断

some - 存在性检查

some 检查数组中是否存在满足条件的元素,只要有一个满足就返回 true

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

// 是否存在偶数
const hasEven = numbers.some(x => x % 2 === 0);
console.log(hasEven); // true

// 是否存在大于 10 的数
const hasLarge = numbers.some(x => x > 10);
console.log(hasLarge); // false

// 实际应用:权限检查
const user = {
permissions: ['read', 'write']
};

const canDelete = user.permissions.some(p => p === 'delete');
console.log(canDelete); // false

// 检查数组中是否有无效项
const items = [{ valid: true }, { valid: false }, { valid: true }];
const hasInvalid = items.some(item => !item.valid);
console.log(hasInvalid); // true

every - 全部性检查

every 检查数组中是否所有元素都满足条件,全部满足才返回 true

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

// 是否所有数字都是正数
const allPositive = numbers.every(x => x > 0);
console.log(allPositive); // true

// 是否所有数字都大于 2
const allGreaterThanTwo = numbers.every(x => x > 2);
console.log(allGreaterThanTwo); // false

// 实际应用:表单验证
const formFields = [
{ name: 'email', value: '[email protected]', valid: true },
{ name: 'password', value: 'password123', valid: true }
];

const canSubmit = formFields.every(field => field.valid);
console.log(canSubmit); // true

// 检查是否所有项都已完成
const todos = [
{ id: 1, text: '学习', completed: true },
{ id: 2, text: '工作', completed: true },
{ id: 3, text: '运动', completed: false }
];

const allCompleted = todos.every(todo => todo.completed);
console.log(allCompleted); // false

// 空数组的特殊情况
console.log([].every(x => x > 0)); // true(空数组默认满足所有条件)
console.log([].some(x => x > 0)); // false(空数组默认不满足存在条件)

flat 和 flatMap - 扁平化

flat - 数组扁平化

flat 将嵌套数组"拉平"成一维数组。

// 基本用法
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // [1, 2, 3, 4, [5, 6]](默认深度 1)
console.log(nested.flat(2)); // [1, 2, 3, 4, 5, 6](深度 2)
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6](完全扁平)

// 实际应用:移除空项
const sparse = [1, , 2, , 3]; // 稀疏数组
console.log(sparse.flat()); // [1, 2, 3]

// 多层嵌套对象
const comments = [
{ text: '评论1', replies: [{ text: '回复1' }, { text: '回复2' }] },
{ text: '评论2', replies: [{ text: '回复3' }] }
];

const allReplies = comments.map(c => c.replies).flat();
// [{ text: '回复1' }, { text: '回复2' }, { text: '回复3' }]

flatMap - 映射后扁平化

flatMapmapflat(1) 的组合,常用于一对多的映射。

// map + flat 的组合
const sentences = ['Hello World', 'Good Morning'];

// 使用 map
const words1 = sentences.map(s => s.split(' '));
// [['Hello', 'World'], ['Good', 'Morning']]

// 使用 flatMap
const words2 = sentences.flatMap(s => s.split(' '));
// ['Hello', 'World', 'Good', 'Morning']

// 实际应用:展开嵌套数据
const orders = [
{ id: 1, items: ['商品A', '商品B'] },
{ id: 2, items: ['商品C'] },
{ id: 3, items: [] }
];

const allItems = orders.flatMap(order => order.items);
// ['商品A', '商品B', '商品C']

// 过滤并映射
const numbers = [1, 2, 3, 4, 5];
const result = numbers.flatMap(x =>
x % 2 === 0 ? [x * 10] : [] // 偶数乘 10,奇数过滤掉
);
console.log(result); // [20, 40]

链式调用

数组方法可以链式组合,构建强大的数据处理管道:

const users = [
{ name: '张三', age: 25, active: true, scores: [80, 90, 85] },
{ name: '李四', age: 30, active: false, scores: [95, 88, 92] },
{ name: '王五', age: 28, active: true, scores: [70, 75, 80] },
{ name: '赵六', age: 35, active: true, scores: [88, 92, 90] }
];

// 链式处理
const result = users
// 1. 过滤活跃用户
.filter(user => user.active)

// 2. 计算平均分并添加到对象
.map(user => ({
...user,
average: user.scores.reduce((a, b) => a + b, 0) / user.scores.length
}))

// 3. 按平均分排序
.sort((a, b) => b.average - a.average)

// 4. 只保留需要的字段
.map(user => ({
name: user.name,
average: user.average.toFixed(1)
}));

console.log(result);
// [
// { name: '赵六', average: '90.0' },
// { name: '张三', average: '85.0' },
// { name: '王五', average: '75.0' }
// ]

复杂数据处理示例

// 处理 API 响应数据
const apiResponse = {
data: {
users: [
{ id: 1, name: '张三', email: '[email protected]', status: 'active' },
{ id: 2, name: '李四', email: '[email protected]', status: 'inactive' },
{ id: 3, name: '王五', email: '', status: 'active' },
{ id: 4, name: '赵六', email: '[email protected]', status: 'active' }
],
meta: { total: 4, page: 1 }
}
};

const processUsers = (response) => {
return response.data.users
// 过滤有效邮箱
.filter(user => user.email && user.email.includes('@'))
// 过滤活跃用户
.filter(user => user.status === 'active')
// 格式化
.map(user => ({
...user,
displayName: user.name.toUpperCase(),
emailDomain: user.email.split('@')[1]
}))
// 按名字排序
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
// 提取最终结构
.map(({ id, displayName, emailDomain }) => ({
id,
name: displayName,
domain: emailDomain
}));
};

console.log(processUsers(apiResponse));
// [
// { id: 4, name: '赵六', domain: 'test.com' },
// { id: 1, name: '张三', domain: 'test.com' }
// ]

性能考虑

避免在回调中做重复计算

const users = [/* 大量数据 */];

// 不好:每次迭代都创建新对象
const result1 = users.map(user => ({
...user,
timestamp: Date.now() // 每次都调用
}));

// 好:提取不变的计算
const timestamp = Date.now();
const result2 = users.map(user => ({
...user,
timestamp
}));

选择合适的方法

// 只需判断存在性时,用 some 而非 find
const hasAdmin = users.some(u => u.role === 'admin'); // 找到就停止
const admin = users.find(u => u.role === 'admin'); // 功能相同但语义不同

// 只需第一个匹配时,用 find 而非 filter
const firstMatch = users.find(u => u.age > 30); // 找到就停止
const allMatches = users.filter(u => u.age > 30); // 遍历全部

// 简单累积时,考虑初始值
const sum = numbers.reduce((a, b) => a + b); // 无初始值,可能出错
const sumSafe = numbers.reduce((a, b) => a + b, 0); // 安全

大数据集的处理

// 对于非常大的数据集,考虑分批处理或使用生成器
function* processInBatches(array, batchSize, processor) {
for (let i = 0; i < array.length; i += batchSize) {
const batch = array.slice(i, i + batchSize);
yield batch.map(processor);
}
}

// 使用
const largeArray = Array(1000000).fill(0).map((_, i) => i);
const batches = processInBatches(largeArray, 10000, x => x * 2);

for (const batch of batches) {
// 处理每批数据
console.log(`处理 ${batch.length} 条数据`);
}

小结

JavaScript 数组方法提供了强大的函数式数据处理能力:

方法用途返回值
map转换每个元素新数组
filter筛选元素新数组
reduce累积计算单个值
find查找元素元素或 undefined
findIndex查找索引索引或 -1
some存在性检查布尔值
every全部性检查布尔值
flat扁平化新数组
flatMap映射后扁平化新数组

使用数组方法的关键原则:

  1. 优先使用数组方法替代 for 循环
  2. 保持回调函数纯净,避免副作用
  3. 合理链式调用,但避免过长链条
  4. 注意性能,大数据集考虑分批处理
  5. 选择语义正确的方法,提高代码可读性

掌握这些数组方法,是成为 JavaScript 高级开发者的必备技能。它们不仅让代码更简洁,还能帮助你更好地理解和处理数据。

参考资源