数组是 JavaScript 中最常用的数据结构之一。从 ES5 到 ES6+,数组方法经历了多次扩充,如今已经非常强大。熟练掌握数组方法,能让你的代码更简洁、更易读、更少出 Bug。本文系统梳理了 20+ 个数组方法,按用途分类讲解,并给出实战示例。
1. 遍历与迭代:forEach、map、filter、reduce
这四个是日常开发中使用频率最高的数组方法,也是函数式编程的基础。
map:转换数组
对数组中每个元素执行转换,返回一个新数组,长度和原数组相同。
const nums = [1, 2, 3, 4, 5];
// 每个元素乘 2
const doubled = nums.map(n => n * 2);
// [2, 4, 6, 8, 10]
// 提取对象属性
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const names = users.map(u => u.name);
// ['Alice', 'Bob']
// 异步 map(注意:map 不会等待 Promise)
const results = await Promise.all(
ids.map(async id => await fetchUser(id))
);
filter:筛选数组
返回满足条件的元素组成的新数组。不改变原数组。
const nums = [1, 2, 3, 4, 5, 6, 7, 8];
// 取偶数
const evens = nums.filter(n => n % 2 === 0);
// [2, 4, 6, 8]
// 过滤空值
const data = [1, null, 2, undefined, 3, ''];
const clean = data.filter(Boolean);
// [1, 2, 3]
// 数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.filter((v, i, a) => a.indexOf(v) === i);
// [1, 2, 3]
reduce:万能转换
reduce 是最强大也最难掌握的数组方法。它可以把数组归约为任意类型的值:数字、对象、甚至另一个数组。
const nums = [1, 2, 3, 4, 5];
// 求和
const sum = nums.reduce((acc, n) => acc + n, 0);
// 15
// 统计次数
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, f) => {
acc[f] = (acc[f] || 0) + 1;
return acc;
}, {});
// { apple: 3, banana: 2, orange: 1 }
// 数组扁平化(单层)
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], []);
// [1, 2, 3, 4, 5]
链式调用
map、filter 都返回数组,因此可以链式调用,写出非常优雅的数据处理流水线:
const result = products
.filter(p => p.price > 100)
.map(p => ({ ...p, finalPrice: p.price * 0.8 }))
.sort((a, b) => b.finalPrice - a.finalPrice)
.slice(0, 10);
2. 查找与判断:find、some、every、includes
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
// find:找到第一个匹配的元素
const alice = users.find(u => u.name === 'Alice');
// { id: 1, name: 'Alice', age: 25 }
// findIndex:找到第一个匹配元素的索引
const idx = users.findIndex(u => u.age > 30);
// 2
// some:是否至少有一个满足条件
const hasSenior = users.some(u => u.age >= 60);
// false
// every:是否所有元素都满足条件
const allAdult = users.every(u => u.age >= 18);
// true
// includes:是否包含某个值(基本类型)
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
小技巧:用 some 配合 ! 替代 every 有时更高效——some 在找到第一个匹配就停止,而 every 需要遍历所有元素。
3. 增删改:push、pop、splice、slice
注意区分:改变原数组的方法(push、pop、shift、unshift、splice、sort、reverse)和不改变原数组的方法(slice、concat、map、filter)。在 React 等不可变数据范式中,优先使用不改变原数组的方法。
const arr = [1, 2, 3, 4, 5];
// push/pop 操作尾部(O(1))
arr.push(6); // [1,2,3,4,5,6]
arr.pop(); // [1,2,3,4,5]
// shift/unshift 操作头部(O(n),性能较差)
arr.unshift(0); // [0,1,2,3,4,5]
arr.shift(); // [1,2,3,4,5]
// slice:切片,不改变原数组
arr.slice(1, 3); // [2, 3]
arr.slice(-2); // [4, 5]
// splice:删除/插入/替换,改变原数组
arr.splice(2, 1); // 删除索引2的1个元素 → [1,2,4,5]
arr.splice(2, 0, 'a'); // 在索引2插入 'a' → [1,2,'a',4,5]
arr.splice(1, 2, 'x','y'); // 替换 → [1,'x','y',4,5]
4. 排序:sort 的正确用法
JavaScript 的 sort 默认按字符串 Unicode 码点排序,这是最容易踩坑的地方:
// ❌ 错误:数字被当成字符串比较
[10, 2, 20, 1].sort();
// [1, 10, 2, 20]
// ✅ 正确:传入比较函数
[10, 2, 20, 1].sort((a, b) => a - b); // 升序
// [1, 2, 10, 20]
[10, 2, 20, 1].sort((a, b) => b - a); // 降序
// [20, 10, 2, 1]
// 对象数组排序
const users = [{name: 'Bob', age: 30}, {name: 'Alice', age: 25}];
users.sort((a, b) => a.age - b.age); // 按年龄升序
// 字符串排序(中文用 localeCompare)
users.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
注意:sort 会改变原数组!如果不想改变原数组,先复制一份:[...arr].sort(...)。
5. 实用技巧与常见坑
数组去重的几种方式
// 1. Set 最简单(基本类型)
const unique = [...new Set(arr)];
// 2. filter + indexOf(保留首次出现位置)
const unique = arr.filter((v, i, a) => a.indexOf(v) === i);
// 3. 对象数组按某个字段去重
const seen = new Set();
const unique = users.filter(u => {
if (seen.has(u.id)) return false;
seen.add(u.id);
return true;
});
常见坑点
- forEach 不能 break:需要中途停止遍历请用 for 循环或 some
- sort 改变原数组:不可变场景先拷贝
- map 中的 async:map 不等待 Promise,需要配合 Promise.all
- 稀疏数组:
new Array(3)创建的是空位数组,map 不会遍历空位 - 数组是引用类型:
arr1 = arr2只是赋值引用,修改会相互影响
更多数组调试技巧,可以使用 DevToolHub 的 JSON 格式化工具查看数组结构,方便地进行格式化和校验。
6. ES2019+ 新增方法
JavaScript 标准一直在演进,近几年新增了不少实用的数组方法,了解它们可以让代码更简洁。
// flat():数组扁平化
const arr = [1, [2, [3, [4]]]];
arr.flat(); // [1, 2, [3, [4]]] 扁平化一层
arr.flat(2); // [1, 2, 3, [4]] 扁平化两层
arr.flat(Infinity); // [1, 2, 3, 4] 全部扁平化
// flatMap():map + flat 的组合,性能更好
const words = ['hello world', 'good morning'];
words.flatMap(w => w.split(' '));
// ['hello', 'world', 'good', 'morning']
// at():支持负索引(终于!)
const arr = [1, 2, 3, 4, 5];
arr.at(-1); // 5 最后一个元素
arr.at(-2); // 4 倒数第二个
arr.at(0); // 1 第一个
// toReversed() / toSorted() / toSpliced() / with()
// ES2023 新增的不可变方法,返回新数组
const arr = [3, 1, 2];
arr.toReversed(); // [2, 1, 3] arr 不变
arr.toSorted(); // [1, 2, 3] arr 不变
arr.toSpliced(1, 1); // [3, 2] arr 不变
arr.with(0, 100); // [100, 1, 2] arr 不变
// findLast() / findLastIndex():从后往前找
const users = [
{ name: 'Alice', active: false },
{ name: 'Bob', active: true },
{ name: 'Charlie', active: true }
];
users.findLast(u => u.active); // { name: 'Charlie', active: true }
users.findLastIndex(u => u.active); // 2
这些新方法中,at() 和不可变数组方法特别实用。at(-1) 终于解决了 JavaScript 数组取最后一个元素不够优雅的问题(不再需要 arr[arr.length - 1])。而 toReversed/toSorted/toSpliced 等不可变方法,让 React 等需要不可变更新的场景写起来更方便。
7. 数组与迭代器
ES6 引入的迭代器和生成器让数组的遍历方式更加灵活。理解迭代器可以帮助你写出更通用的代码。
// 三个返回迭代器的方法
const arr = ['a', 'b', 'c'];
// keys() - 键的迭代器
[...arr.keys()]; // [0, 1, 2]
// values() - 值的迭代器
[...arr.values()]; // ['a', 'b', 'c']
// entries() - 键值对的迭代器
[...arr.entries()]; // [[0,'a'], [1,'b'], [2,'c']]
// for...of + entries 同时获取索引和值
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
// 类数组转数组
Array.from(arrayLike); // 通用方法
Array.from('hello'); // ['h','e','l','l','o']
Array.from({length: 5}, (_, i) => i * 2); // [0, 2, 4, 6, 8]
// Array.of - 创建数组(解决 Array(3) 的歧义)
Array.of(3); // [3]
Array.of(1,2,3); // [1, 2, 3]
8. 性能考量
数组方法虽然方便,但在处理超大规模数据时需要考虑性能。以下是一些性能优化建议:
- 避免不必要的中间数组:链式调用 map+filter 会创建多个中间数组。大数据量时可以用 reduce 一次完成。
- pre-allocate 数组:已知长度时用
new Array(n)预分配,比反复 push 更快。 - 用 for 循环代替 forEach:for 循环比 forEach 快约 20-50%,超大数据时差异明显。
- Set 用于查找:需要频繁判断元素是否存在时,用 Set 的 has() 比数组的 includes() 快得多(O(1) vs O(n))。
- 避免在循环中修改数组:shift/unshift/splice 在数组头部操作是 O(n) 的,大数据量时很慢。
不过,过早优化是万恶之源。大多数场景下,代码的可读性比那点性能差异重要得多。只有在确实遇到性能瓶颈时,再考虑用性能更好但可读性稍差的写法。
9. 类型化数组(TypedArray)
处理大量二进制数据时,普通数组的性能和内存占用都不理想。ES6 引入的类型化数组可以操作原始二进制数据,性能更好。
// 创建 8 位无符号整数数组(每个元素 1 字节)
const uint8 = new Uint8Array(1024);
uint8[0] = 255;
// 其他类型
new Int8Array(10); // 8位有符号整数
new Uint16Array(10); // 16位无符号整数
new Int32Array(10); // 32位有符号整数
new Float32Array(10); // 32位浮点数
new Float64Array(10); // 64位浮点数(和普通 Number 一样)
// 从 ArrayBuffer 创建
const buffer = new ArrayBuffer(16); // 16 字节
const view = new DataView(buffer);
view.setUint8(0, 255);
view.setUint32(1, 0x12345678);
view.getFloat64(8);
类型化数组在处理文件、Canvas 像素数据、WebSocket 二进制消息、WebGL 数据等场景中非常重要。它们不是普通数组——没有 push/pop 方法,长度固定,但访问速度更快,内存占用更小。
10. 常见面试题
数组相关的面试题层出不穷,以下是几道经典题目和解题思路:
// 1. 数组去重
const unique = [...new Set(arr)]; // 最简单,基本类型
// 2. 数组扁平化
const flatten = arr => arr.flat(Infinity); // ES2019
// 递归版
const flatten = arr => arr.reduce(
(acc, v) => acc.concat(Array.isArray(v) ? flatten(v) : v),
[]
);
// 3. 数组交集
const intersection = (a, b) => {
const set = new Set(b);
return a.filter(x => set.has(x));
};
// 4. 数组并集
const union = (a, b) => [...new Set([...a, ...b])];
// 5. 数组差集(A 有但 B 没有)
const difference = (a, b) => {
const set = new Set(b);
return a.filter(x => !set.has(x));
};
// 6. 找出数组中出现次数最多的元素
function mostFrequent(arr) {
const map = new Map();
let max = 0, result = null;
for (const item of arr) {
const count = (map.get(item) || 0) + 1;
map.set(item, count);
if (count > max) {
max = count;
result = item;
}
}
return result;
}
掌握这些基础操作,再面对更复杂的数组问题时就能游刃有余。面试中考察数组,核心是考察你对数据结构和算法复杂度的理解,而不是死记硬背 API。
11. 数组与对象的转换
实际开发中,数组和对象经常需要互相转换。掌握这些模式可以少写很多循环。
// 对象 → 数组
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj); // ['a', 'b', 'c']
Object.values(obj); // [1, 2, 3]
Object.entries(obj); // [['a',1], ['b',2], ['c',3]]
// 数组 → 对象(用 Object.fromEntries)
const entries = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(entries); // { a: 1, b: 2, c: 3 }
// 数组对象按 id 建索引(便于查找)
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const userMap = new Map(users.map(u => [u.id, u]));
userMap.get(2); // {id: 2, name: 'Bob'} O(1) 查找
// 对象数组按字段分组
const items = [
{ category: 'fruit', name: 'apple' },
{ category: 'vegetable', name: 'carrot' },
{ category: 'fruit', name: 'banana' }
];
const grouped = items.reduce((acc, item) => {
const key = item.category;
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
// {
// fruit: [{name:'apple'}, {name:'banana'}],
// vegetable: [{name:'carrot'}]
// }
特别是按某个字段建立 Map 索引,在需要频繁查找时非常有用。把 O(n) 的查找变成 O(1),是很常用的性能优化技巧。
12. 数组的深浅拷贝
数组是引用类型,赋值只是复制了引用,修改一个会影响另一个。实际开发中经常需要复制数组。
// 浅拷贝(只复制一层)
const arr = [1, 2, 3];
const copy1 = [...arr]; // 展开运算符
const copy2 = arr.slice(); // slice 不传参
const copy3 = Array.from(arr); // Array.from
const copy4 = arr.concat(); // concat 空数组
// 深拷贝(嵌套数组/对象也会被复制)
const nested = [[1, 2], [3, 4], { a: 1 }];
// 1. JSON 方法(简单场景,但不能处理函数、undefined、循环引用)
const deep1 = JSON.parse(JSON.stringify(nested));
// 2. structuredClone(现代浏览器原生支持,推荐)
const deep2 = structuredClone(nested);
// 3. 递归实现
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(deepClone);
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
structuredClone 是 ES2022 新增的 API,原生支持深拷贝,能处理 Date、RegExp、Map、Set、ArrayBuffer 等类型,比 JSON 方法功能强得多。如果不需要兼容 IE,强烈推荐使用。
13. 用迭代器处理异步流
异步迭代器(Async Iterator)是处理异步数据流的强大工具,特别适合分页加载、流式数据等场景。
// 异步生成器:逐页获取数据
async function* fetchAllPages() {
let page = 1;
while (true) {
const data = await fetch(`/api/items?page=${page}`).then(r => r.json());
if (data.items.length === 0) break;
yield* data.items; // 逐个产出数据项
page++;
}
}
// 使用 for await...of 遍历
async function processAll() {
for await (const item of fetchAllPages()) {
console.log('处理:', item);
}
}
异步迭代器让异步数据流的处理变得和同步数组一样优雅。配合 fetch 的 ReadableStream、数据库游标等流式 API,可以写出非常简洁的异步数据处理代码。这也是 JavaScript 处理大数据量时的一个重要模式。
14. 数组方法的性能对比
你可能好奇,这么多数组方法哪个更快?实际测试下来,for 循环 > forEach > map/filter > reduce。但这个差异在数据量小于一万条时几乎可以忽略,真正有明显差异是在十万、百万级别的数据上。
所以日常开发中,优先选择语义最清晰、可读性最好的写法。等真正遇到性能问题时,再去做针对性优化。毕竟,代码是写给人看的,顺便给机器执行。