JavaScript 数组方法全景:会查、会改、会遍历、会选择
数组几乎是前端里使用频率最高的数据结构之一。你每天都会遇到这些需求:
- 把接口返回的数据过滤一遍
- 找到某个满足条件的元素
- 统计总价、总数、词频
- 向数组头尾插入或删除数据
- 排序、去重、扁平化、转字符串
数组方法很多,但真正要掌握的不是“死记 API 名称”,而是三件事:
- 这个方法会不会修改原数组
- 这个方法返回什么
- 这个方法更适合“查”“改”“变换”还是“聚合”
这篇文档的目标是:用一张方法地图 + 一组高频示例,把常用数组方法一次讲清楚。
1. 先建立总图:数组方法最重要的分类
很多同学一上来就背方法名,结果越背越乱。更高效的记法是先分成两大类:
- 修改原数组
- 不修改原数组
1.1 会修改原数组的方法
| 方法 | 作用 | 返回值 |
|---|---|---|
push() | 末尾追加 | 新长度 |
pop() | 删除末尾元素 | 被删除的元素 |
shift() | 删除开头元素 | 被删除的元素 |
unshift() | 开头插入 | 新长度 |
splice() | 任意位置插入 / 删除 / 替换 | 被删除元素组成的新数组 |
sort() | 排序 | 原数组本身 |
reverse() | 反转 | 原数组本身 |
fill() | 用固定值填充区间 | 原数组本身 |
copyWithin() | 把一段内容复制到另一位置 | 原数组本身 |
1.2 不修改原数组的方法
| 方法 | 作用 | 返回值 |
|---|---|---|
slice() | 截取一段 | 新数组 |
concat() | 合并数组 | 新数组 |
map() | 映射转换 | 新数组 |
filter() | 条件过滤 | 新数组 |
flat() | 扁平化 | 新数组 |
flatMap() | 映射后再拍平一层 | 新数组 |
find() | 找到第一个满足条件的元素 | 元素或 undefined |
findIndex() | 找到第一个满足条件元素的下标 | 下标或 -1 |
includes() | 判断是否包含某值 | boolean |
some() | 是否至少一个满足条件 | boolean |
every() | 是否全部满足条件 | boolean |
reduce() | 聚合成一个结果 | 累加结果 |
join() | 拼接字符串 | 字符串 |
toSorted() | 排序,但不改原数组 | 新数组 |
toReversed() | 反转,但不改原数组 | 新数组 |
toSpliced() | splice 的不变版本 | 新数组 |
with() | 替换某一项,但不改原数组 | 新数组 |
会不会改原数组,是数组题里最容易出 bug 的分界线。
2. 创建与识别数组
2.1 Array.isArray():判断是不是数组
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray('abc')); // false
console.log(Array.isArray({ length: 2 })); // false
为什么不用 typeof?
因为:
typeof []; // "object"
所以判断数组最稳妥的方式就是 Array.isArray()。
2.2 Array.from():把“类数组 / 可迭代对象”转成真正数组
const set = new Set([1, 2, 3]);
const arr1 = Array.from(set);
const arr2 = Array.from({ length: 5 }, (_, i) => i * 2);
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [0, 2, 4, 6, 8]
常见用途:
- 把
Set转数组 - 把
NodeList转数组 - 生成固定长度数组
2.3 Array.of():按参数原样创建数组
console.log(Array.of(3)); // [3]
console.log(Array(3)); // [ <3 empty items> ]
这一点很重要:Array(3) 表示“长度为 3 的空数组”,而不是 [3]。
3. 读取、查找与判断
3.1 at():按位置读取,支持负索引
const arr = ['a', 'b', 'c', 'd'];
console.log(arr.at(0)); // 'a'
console.log(arr.at(-1)); // 'd'
console.log(arr.at(-2)); // 'c'
相比 arr[arr.length - 1],at(-1) 更直观。
3.2 includes():判断某个值在不在
const arr = [1, 2, 3, NaN];
console.log(arr.includes(2)); // true
console.log(arr.includes(9)); // false
console.log(arr.includes(NaN)); // true
如果你的目标只是“是否存在这个值”,优先考虑 includes()。
3.3 indexOf() / lastIndexOf():找到值的位置
const arr = ['a', 'b', 'c', 'b'];
console.log(arr.indexOf('b')); // 1
console.log(arr.lastIndexOf('b')); // 3
console.log(arr.indexOf('x')); // -1
它适合找“某个确定值”的位置,但如果你要按条件查找,应该换 find 系列。
3.4 find() / findIndex():按条件找
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' },
];
const user = users.find((item) => item.id === 2);
const index = users.findIndex((item) => item.id === 2);
console.log(user); // { id: 2, name: '李四' }
console.log(index); // 1
你可以这样记:
- 找元素本身:
find() - 找元素下标:
findIndex()
3.5 findLast() / findLastIndex():从后往前找
const nums = [1, 4, 7, 8, 10];
console.log(nums.findLast((n) => n % 2 === 0)); // 10
console.log(nums.findLastIndex((n) => n % 2 === 0)); // 4
这两个方法在“找最后一个满足条件的数据”时比先 reverse() 再找更安全,因为它们不会改原数组。
3.6 some() / every():做条件判断
const scores = [88, 92, 76, 95];
console.log(scores.some((n) => n < 80)); // true
console.log(scores.every((n) => n >= 60)); // true
你可以把它们理解成:
some():至少一个every():全部都要
3.7 一张表快速区分
| 需求 | 推荐方法 |
|---|---|
| 判断值是否存在 | includes() |
| 找某个值第一次出现的位置 | indexOf() |
| 找第一个满足条件的元素 | find() |
| 找第一个满足条件元素的下标 | findIndex() |
| 找最后一个满足条件的元素 | findLast() |
| 是否至少一个满足条件 | some() |
| 是否全部满足条件 | every() |
4. 遍历、转换与聚合
4.1 forEach():只遍历,不产出新数组
const arr = [1, 2, 3];
arr.forEach((item, index) => {
console.log(index, item);
});
特点:
- 适合“遍历执行副作用”,比如打印日志、改 DOM、发埋点
- 没有返回值价值,不要指望它生成新数组
4.2 map():一一映射,得到等长新数组
const prices = [10, 20, 30];
const doubled = prices.map((n) => n * 2);
console.log(doubled); // [20, 40, 60]
适合场景:
- 后端字段改名
- 数据格式转换
- 给列表增加展示字段
map() 最常见的坑回调里忘了 return,结果会得到一个装满 undefined 的新数组。
4.3 filter():按条件筛选
const nums = [1, 2, 3, 4, 5, 6];
const evenNums = nums.filter((n) => n % 2 === 0);
console.log(evenNums); // [2, 4, 6]
它不会改原数组,只会把满足条件的项收集出来。
4.4 flat():把嵌套数组拍平
const arr = [1, [2, 3], [4, [5, 6]]];
console.log(arr.flat()); // [1, 2, 3, 4, [5, 6]]
console.log(arr.flat(2)); // [1, 2, 3, 4, 5, 6]
默认只拍平一层。
4.5 flatMap():先映射,再拍平一层
const sentences = ['hello world', 'js array'];
const words = sentences.flatMap((item) => item.split(' '));
console.log(words); // ['hello', 'world', 'js', 'array']
如果你本来要写:
arr.map(...).flat();
那么可以考虑直接用 flatMap()。
4.6 reduce():把多个值汇总成一个结果
reduce() 是数组方法里最强、也最容易让初学者绕晕的方法。它的核心只有一句话:
把数组中的每一项不断“累计”起来,最后得到一个结果。
const nums = [1, 2, 3, 4];
const total = nums.reduce((sum, n) => sum + n, 0);
console.log(total); // 10
参数含义:
sum:累计值n:当前项0:初始值
常见用途:
- 求和、求平均值
- 统计词频
- 按字段分组
- 把数组转对象
统计词频示例:
const words = ['js', 'css', 'js', 'html', 'js'];
const countMap = words.reduce((acc, word) => {
acc[word] = (acc[word] || 0) + 1;
return acc;
}, {});
console.log(countMap);
// { js: 3, css: 1, html: 1 }
4.7 reduceRight():从右往左聚合
const arr = ['a', 'b', 'c'];
const result = arr.reduceRight((acc, cur) => acc + cur, '');
console.log(result); // 'cba'
它不常用,但在“从右往左组合”时有价值。
5. 增删改:什么时候该用哪一个?
5.1 push() / pop():操作数组尾部
const arr = [1, 2];
arr.push(3);
console.log(arr); // [1, 2, 3]
const last = arr.pop();
console.log(last); // 3
console.log(arr); // [1, 2]
这是最常见、也通常性能最友好的头尾操作之一。
5.2 shift() / unshift():操作数组头部
const arr = [2, 3];
arr.unshift(1);
console.log(arr); // [1, 2, 3]
const first = arr.shift();
console.log(first); // 1
console.log(arr); // [2, 3]
语义很直观,但大量头部插入 / 删除通常没有尾部操作高效。
5.3 splice():最灵活的“手术刀”
splice(start, deleteCount, ...items) 可以同时做三件事:
- 删除
- 插入
- 替换
const arr = ['a', 'b', 'c', 'd'];
arr.splice(1, 2, 'x', 'y');
console.log(arr); // ['a', 'x', 'y', 'd']
这行代码的含义是:
- 从下标
1开始 - 删除
2项 - 再插入
'x'、'y'
5.4 slice():截取一段,但不改原数组
const arr = ['a', 'b', 'c', 'd'];
const part = arr.slice(1, 3);
console.log(part); // ['b', 'c']
console.log(arr); // ['a', 'b', 'c', 'd']
你可以这样硬记:
splice有字母 p,像“patch”,会动原数组slice像“切一片出来”,不动原数组
5.5 concat():合并数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2, [5, 6]);
console.log(merged); // [1, 2, 3, 4, 5, 6]
它不会修改原数组。
5.6 fill():批量填充值
const arr = new Array(5).fill(0);
console.log(arr); // [0, 0, 0, 0, 0]
arr.fill(9, 1, 4);
console.log(arr); // [0, 9, 9, 9, 0]
很适合:
- 初始化固定长度数组
- 批量占位
fill() 的引用类型坑如果用对象去 fill,每一项会指向同一个对象引用。
const arr = new Array(3).fill({ done: false });
arr[0].done = true;
console.log(arr);
// [{ done: true }, { done: true }, { done: true }]
5.7 copyWithin():把数组内部一段复制到另一段
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3);
console.log(arr); // [4, 5, 3, 4, 5]
这个方法不常作为业务主力,但你至少要知道它:
- 会修改原数组
- 是“数组内部复制”,不是扩容
6. 排序、反转与“不变写法”
6.1 reverse():原地反转
const arr = [1, 2, 3];
arr.reverse();
console.log(arr); // [3, 2, 1]
注意:它会改原数组。
6.2 sort():默认按字符串排序
这是数组题里的头号高频坑。
const nums = [1, 10, 2, 20];
nums.sort();
console.log(nums); // [1, 10, 2, 20]
为什么不是 [1, 2, 10, 20]?
因为默认会先转成字符串,再按字典序比较。
数字排序要这样写:
const nums = [1, 10, 2, 20];
nums.sort((a, b) => a - b); // 升序
console.log(nums); // [1, 2, 10, 20]
nums.sort((a, b) => b - a); // 降序
console.log(nums); // [20, 10, 2, 1]
sort 的比较函数怎么记a - b:升序b - a:降序
6.3 toSorted() / toReversed() / toSpliced() / with():现代不变写法
这组方法很值得掌握,因为它们兼顾了“语义清晰”和“避免副作用”。
const arr = [3, 1, 2];
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const replaced = arr.with(1, 99);
const spliced = arr.toSpliced(1, 1, 88);
console.log(arr); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
console.log(reversed); // [2, 1, 3]
console.log(replaced); // [3, 99, 2]
console.log(spliced); // [3, 88, 2]
它们特别适合:
- React / Vue 这类强调不可变数据的场景
- 你不想在多人协作里引入“原地修改”的隐式副作用
这几个新方法属于较新的 ECMAScript 能力。旧运行环境里要提前确认兼容性,必要时配合构建工具或 Polyfill。
7. 转字符串与迭代器方法
7.1 join():把数组拼成字符串
const arr = ['2026', '03', '09'];
console.log(arr.join('-')); // '2026-03-09'
console.log(arr.join('')); // '20260309'
7.2 toString():默认用逗号连接
console.log([1, 2, 3].toString()); // '1,2,3'
日常开发里更常主动用 join(),因为分隔符可控。
7.3 keys() / values() / entries():拿迭代器
const arr = ['a', 'b', 'c'];
for (const index of arr.keys()) {
console.log(index);
}
for (const value of arr.values()) {
console.log(value);
}
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
它们适合在 for...of 里配合使用。
8. 高频坑点:比“会写”更重要
8.1 sort() 默认不是数字排序
这是最容易考、最容易写错的一题:
[2, 11, 3].sort(); // [11, 2, 3]
8.2 slice() 和 splice() 经常混
slice():不改原数组splice():改原数组
8.3 forEach() 不能 break,也不适合直接配 await
arr.forEach(async (item) => {
await fetchItem(item);
});
这段代码常见问题是:你以为它会“一个接一个等”,但实际上 forEach 不会等待整体完成。
如果你需要串行异步,更常见的是:
for (const item of arr) {
await fetchItem(item);
}
8.4 reduce() 最好总是写初始值
const sum = [1, 2, 3].reduce((acc, cur) => acc + cur, 0);
这样更清晰,也能避免空数组时报错。
8.5 浅拷贝不是深拷贝
slice()、concat()、展开运算符 ... 都只是浅拷贝。
const arr = [{ count: 1 }];
const copied = arr.slice();
copied[0].count = 99;
console.log(arr[0].count); // 99
因为里面的对象引用还是同一个。
8.6 稀疏数组(空位)行为不完全一样
const arr = [1, , 3];
arr.map((x) => x * 2); // [2, empty, 6]
arr.includes(undefined); // true
arr.indexOf(undefined); // -1
这说明:
- 有些方法会“跳过空位”
- 有些方法会把空位当成
undefined处理
日常开发里最稳妥的做法是:尽量不要主动制造稀疏数组。
9. 实战里怎么选:一套够用判断法
如果你只想快速选方法,可以直接套下面这张表:
| 你现在的需求 | 优先考虑 |
|---|---|
| 我只想知道某个值在不在 | includes() |
| 我想找到符合条件的第一项 | find() |
| 我想保留符合条件的所有项 | filter() |
| 我想把每一项转成新结构 | map() |
| 我想把数组合成一个值 | reduce() |
| 我想复制一段数组 | slice() |
| 我想在任意位置增删改 | splice() |
| 我想排序但不污染原数组 | toSorted() |
| 我想改某一项但不污染原数组 | with() |
| 我想拿最后一个元素 | at(-1) |
高频面试题(含参考答案)
Q1:map() 和 forEach() 的区别是什么?
答: map() 会返回一个新数组,适合“数据转换”;forEach() 主要用于遍历执行副作用,不适合拿来产出新数组。
Q2:slice() 和 splice() 的区别是什么?
答: slice() 用来截取数组片段,不修改原数组;splice() 用来在任意位置删除、插入、替换元素,会修改原数组。
Q3:为什么 [10, 2, 1].sort() 的结果不是数字升序?
答: 因为 sort() 默认按字符串字典序比较,不是按数字大小比较。数字排序要写比较函数,如 arr.sort((a, b) => a - b)。
Q4:find() 和 filter() 的区别是什么?
答: find() 返回第一个满足条件的元素,找到就停;filter() 会遍历完整个数组,把所有满足条件的元素组成一个新数组。
Q5:includes() 和 indexOf() 都能判断存在,怎么选?
答: 如果你只关心“在不在”,优先 includes(),语义更直接,而且它能正确判断 NaN;如果你还需要位置,再考虑 indexOf()。
Q6:reduce() 的本质是什么?
答: 本质是“累计器模式”。它会把数组每一项不断累积到一个结果里,这个结果可以是数字、对象、数组、Map,甚至任意复杂结构。
Q7:哪些数组方法会修改原数组?
答: 高频需要记住的有:push、pop、shift、unshift、splice、sort、reverse、fill、copyWithin。
Q8:为什么在 React 里更推荐 toSorted()、toSpliced()、with() 这类写法?
答: 因为它们不会原地修改原数组,更符合不可变数据的思路,状态变化更清晰,也更不容易引入副作用。