🔥 一文掌握 JavaScript 数组方法(2025 全面指南):分类解析 × 业务场景 × 易错点
本文目标:让你在业务里正确选用数组方法 ,写出可维护、无副作用、性能靠谱 的代码。
适用对象:从前端入门到高级进阶,尤其是写 React/Vue/Node 的同学。
1)概览:如何选用数组方法
第一原则:能用纯函数就不用会改原数组的方法。
在状态管理(React/Vue/Redux/MobX)与多人协作中,副作用常是 Bug 根源。
- ✅ 纯函数(不改原数组) :
map/filter/slice/concat/flat/reduce/find/some/every/includes/indexOf/findLast*/at/ ES2023 的toSorted/toReversed/toSpliced/with - ⚠️ 会修改原数组 :
push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin
想到这里:有"toXxx" 就是"返回新数组的版本" (ES2023"Change-Array-by-Copy"提案带来的)。
2)会修改原数组(非纯函数,慎用⚠️)
2.1 栈/队列:push / pop / unshift / shift
-
是否修改:✅ 修改
-
复杂度 :
push/pop→ 摊还 O(1);unshift/shift→ O(n)(头部移动会搬迁元素) -
业务场景
- 聊天消息流:新消息
push到尾部;撤回最后一条pop - 滑动窗口:
push添加,超长后shift丢弃头部(注意性能)
- 聊天消息流:新消息
-
易错点
- 高频
unshift/shift可能退化性能(每次操作搬迁 n 个元素)
- 高频
-
最佳实践
- 需要频繁头部操作 → 用双端队列 (如
deque数据结构)或 链表,或转成环形缓冲
- 需要频繁头部操作 → 用双端队列 (如
scss
// ✅ React 状态中避免直接 push
setList(prev => [...prev, newItem]); // 纯函数写法
2.2 通用改动:splice(start, deleteCount, ...items)
-
是否修改:✅ 修改
-
返回值:被删除的元素数组
-
复杂度:O(n)(可能移动大量元素)
-
场景:购物车删除/替换商品;表格行插入/删除
-
易错点
delete arr[i]会留下空洞 (sparse),不要用它删除,改用splice
-
纯函数替代 :
toSpliced(ES2023)或slice拼接
scss
// 删除索引 idx 处元素(纯函数)
const removeAt = (arr, idx) => arr.slice(0, idx).concat(arr.slice(idx + 1));
2.3 排序/反转:sort(compareFn) / reverse()
-
是否修改:✅ 修改
-
复杂度:O(n log n)
-
场景:商品按价/热度排序;时间线反转
-
易错点
sort()不传比较函数时按字典序 :[2,10,3].sort() // [10,2,3]sort自 ES2019 起规范要求稳定排序(大多数现代引擎实现稳定)
-
国际化
- 中文/多语言排序用
Intl.Collator或localeCompare
- 中文/多语言排序用
-
纯函数替代 :
toSorted/toReversed
css
prices.toSorted((a, b) => a - b); // 数字升序(不改原数组)
names.toSorted((a, b) => a.localeCompare(b, 'zh-Hans-u-co-pinyin'));
2.4 就地填充/复制:fill / copyWithin
-
是否修改:✅ 修改
-
场景:初始化缓存区;滑动窗口构造
-
易错点
Array(3).fill({})是同一个对象引用三次- 需要不同对象:
Array.from({length:3}, () => ({}))
3)不修改原数组(纯函数,推荐✅)
3.1 遍历与映射:forEach / map
-
是否修改:❌ 不改(但你在回调里若改外部对象,依然有副作用)
-
返回值 :
forEach→undefined;map→ 新数组 -
场景:接口数据转展示结构;字段格式化
-
易错点
forEach不能使用break/return提前结束 ;可用some/every或普通for循环
-
最佳实践
- 链式调用 :
filter().map().reduce();注意内存(会生成中间数组)
- 链式调用 :
php
// 列表转 UI 选项
const options = users.map(u => ({ label: u.name, value: u.id }));
3.2 查找类:find / findIndex / findLast / findLastIndex / includes / indexOf / lastIndexOf / at
- **是否修改**:❌ 不改
-
返回值:元素/索引/布尔
-
场景
- RBAC 权限:
roles.includes('admin') - 搜索最近匹配项:日志/时间线用
findLast* - 负索引访问:
arr.at(-1)取最后一个
- RBAC 权限:
-
易错点
includes与indexOf的 NaN 行为不同 :
includes(NaN)→true;indexOf(NaN)→-1- 没有
any这个方法,判断"存在任意满足项"请用some
-
对比
find找值;findIndex找索引;找不到分别为undefined/-1includes适合纯等值 (SameValueZero);find适合条件判断
3.3 过滤 & 判定:filter / some / every
-
是否修改:❌ 不改
-
复杂度:O(n)
-
场景
- 列表筛选:库存 > 0、状态为启用
- 表单校验:
every(field => !!field.value) - 早停逻辑:
some/every可短路(性能友好)
3.4 拼接 & 裁剪:concat / slice
-
是否修改:❌ 不改
-
场景
- 无限滚动分页:
list = list.concat(nextPage) - 复制数组:
const copy = arr.slice()(或[...arr])
- 无限滚动分页:
-
易错点
slice的end不含本身(半开区间)
3.5 扁平化与映射:flat / flatMap
-
是否修改:❌ 不改
-
场景
- 树形菜单拍平渲染;评论楼层降维
-
细节
flat(depth=1);flatMap(fn)等价arr.map(fn).flat(1)flat会移除空洞(sparse entry)
3.6 归约:reduce
-
是否修改:❌ 不改
-
场景
- 汇总统计、分组、索引化(
id -> itemMap)
- 汇总统计、分组、索引化(
-
易错点
- 务必传初始值 ,否则空数组会抛
TypeError
- 务必传初始值 ,否则空数组会抛
-
配方
- 求和:
arr.reduce((s,x)=>s+x, 0) - 分组:见第 7 节"业务实战"
- 求和:
4)ES6+ / ES2023+ 新方法(🆕 非破坏性更安全)
以下方法极大改善了"不要改原数组"的开发体验,尤其适合 React/Vue 状态更新。
-
ES2022
at(index):支持负索引读:arr.at(-1)
-
ES2023(变更复制家族,推荐)
toSorted(compareFn):像sort,但返回新数组toReversed():像reverse,但返回新数组toSpliced(start, deleteCount, ...items):像splice,但返回新数组with(index, value):返回替换后新数组(不会改原数组)
-
ES2023(查找)
findLast/findLastIndex:从尾到头搜索最近匹配项
迁移建议:统一替换 项目里对
sort/reverse/splice的直接调用,避免无意改动原数组导致的状态异常。
5)迭代器与遍历语法选择
for...of:按值遍历(尊重迭代器),推荐for...in:遍历可枚举属性键(含原型链),不推荐用于数组entries()/keys()/values():获得显式迭代器- 生成器/惰性序列:在超大数据流里避免中间数组(进阶)
css
for (const [i, v] of arr.entries()) {
// i: 索引,v: 值
}
6)静态方法 & 工具化技巧
注意辨别:下面第一行才是"静态方法(挂在 Array 上)" ;很多易混的其实是"原型方法"。
-
静态方法(Array.*)
Array.isArray(x):跨 iframe 也准确Array.from(iterable, mapFn?, thisArg?):把可迭代/类数组转真数组;遇空洞会转为undefined(例如[...[,1,,2]])Array.of(...items):参数即元素(避免Array(3)语义歧义)
-
原型方法但常被误认是静态的(且会改动!)
fill、copyWithin(会修改原数组)includes是原型方法 (不是Array.includes)
-
工具化小技
-
稳妥初始化不同对象:
iniconst list = Array.from({ length: 3 }, () => ({}));
-
7)业务实战配方(复制即用)
7.1 去重
dart
// 基础类型
const unique = arr => [...new Set(arr)];
// 对象按字段去重
const uniqueBy = (arr, key) => {
const seen = new Set();
return arr.filter(item => (seen.has(item[key]) ? false : (seen.add(item[key]), true)));
};
7.2 分组(groupBy)
ini
const groupBy = (arr, keyFn) => arr.reduce((acc, cur) => {
const k = keyFn(cur);
(acc[k] ||= []).push(cur);
return acc;
}, Object.create(null));
// 用法:按城市分组用户
const groups = groupBy(users, u => u.city);
7.3 建索引(id → item)
javascript
const indexBy = (arr, keyFn) =>
arr.reduce((m, x) => (m.set(keyFn(x), x), m), new Map());
7.4 幂等更新(不可变写法)
ini
// 更新 id=3 的条目
const updateOne = (list, id, patch) =>
list.map(item => item.id === id ? { ...item, ...patch } : item);
7.5 插入/删除(不可变)
ini
const insertAt = (arr, idx, x) => arr.slice(0, idx).concat(x, arr.slice(idx));
const removeAt = (arr, idx) => arr.slice(0, idx).concat(arr.slice(idx + 1));
7.6 排序(可读 & 国际化)
scss
const by = (...fns) => (a, b) => {
for (const f of fns) {
const d = f(a, b);
if (d) return d;
}
return 0;
};
const asc = sel => (a, b) => (sel(a) > sel(b)) - (sel(a) < sel(b));
const desc = sel => (a, b) => (sel(b) > sel(a)) - (sel(b) < sel(a));
products.toSorted(by(asc(p => p.price), desc(p => p.rating)));
7.7 集合运算(并/交/差)
javascript
const union = (a, b) => [...new Set([...a, ...b])];
const intersection= (a, b) => { const s=new Set(b); return a.filter(x => s.has(x)); };
const difference = (a, b) => { const s=new Set(b); return a.filter(x => !s.has(x)); };
7.8 分页拼接与去重
ini
const mergePage = (prev, next, key) => {
const map = new Map();
[...prev, ...next].forEach(x => map.set(x[key], x));
return [...map.values()];
};
7.9 安全求和/平均/中位数
ini
const sum = (arr, sel = x => x) => arr.reduce((s,x)=>s+sel(x), 0);
const avg = (arr, sel = x => x) => arr.length ? sum(arr, sel)/arr.length : 0;
const median= (arr, sel = x => x) => {
const a = arr.map(sel).toSorted((x,y)=>x-y);
const n = a.length, mid = n>>1;
return n ? (n%2 ? a[mid] : (a[mid-1]+a[mid])/2) : 0;
};
8)性能与复杂度(Big-O)常识(简版)
| 类别 | 方法 | 时间复杂度 | 备注 |
|---|---|---|---|
| 访问 | at, 索引读取 |
O(1) | 负索引仅语法层便捷 |
| 增删 | push/pop |
摊还 O(1) | 末端操作高效 |
| 增删 | shift/unshift/splice |
O(n) | 头部/中间会搬迁 |
| 遍历 | forEach/map/filter/reduce/some/every |
O(n) | some/every 可早停 |
| 查找 | find/indexOf/includes/findLast* |
O(n) | includes 支持 NaN |
| 排序 | sort/toSorted |
O(n log n) | ES2019 起应稳定 |
| 扁平 | flat/flatMap |
O(n) | flat 移除空洞 |
链式调用 可读性强但会创建中间数组;超大数据可考虑生成器/惰性管道 或把逻辑合并到单次
reduce。
9)易错点与坑位图(强烈建议收藏)
-
sort默认字典序 → 数字排序一定传比较函数 -
includes(NaN)为真,indexOf(NaN)为 -1 -
没有
any方法 → 用some表达"存在任意满足项" -
删除元素不要用
delete→ 留空洞;请用splice或纯函数toSpliced/slice拼接 -
forEach不能break→ 换some/every或for...of -
Array(3).fill({})同引用 → 想要不同对象用Array.from({length:3}, ()=> ({})) -
React/Vue 状态不可变 → 禁止直接
push/splice/sort;用toSorted/toSpliced或扩展运算符 -
稀疏数组与空洞
map/forEach跳过空洞;flat会移除空洞;Array.from([...])/...展开会把空洞转成undefined
-
国际化排序 用
Intl.Collator或localeCompare,不要自己写全量拼音表 -
reduce一定写初始值,否则空数组崩
10)数组方法大速查表(可打印/收藏)
注:✅ 修改 = 会改原数组;❌ 修改 = 不改原数组;返回值仅列核心。
| 维度 | 方法 | 修改原数组 | 返回值 | 典型用途 |
|---|---|---|---|---|
| 栈/队 | push/pop |
✅ | 新长度 / 弹出项 | 消息流末端增删 |
| 头部 | unshift/shift |
✅ | 新长度 / 弹出项 | 队列头操作(慎用) |
| 改动 | splice |
✅ | 删除项数组 | 中间插入/删除/替换 |
| 排序 | sort/reverse |
✅ | 排序后原数组 | 排序/反转(慎用) |
| 填充 | fill |
✅ | 原数组 | 初始化、重置 |
| 复制 | copyWithin |
✅ | 原数组 | 局部覆盖复制 |
| 遍历 | forEach |
❌ | undefined |
副作用遍历(统计/日志) |
| 转换 | map |
❌ | 新数组 | 字段映射、UI 适配 |
| 过滤 | filter |
❌ | 子集 | 列表筛选 |
| 判定 | some/every |
❌ | boolean |
存在任意/全部满足 |
| 查找 | find/findIndex |
❌ | 元素/索引 | 从左找首个匹配 |
| 查找 | findLast/...Index |
❌ | 元素/索引 | 从右找最近匹配 |
| 包含 | includes |
❌ | boolean |
是否含某值(含 NaN) |
| 位置 | indexOf/lastIndexOf |
❌ | 索引/-1 | 值等值匹配(不含 NaN) |
| 获取 | at |
❌ | 值/undefined |
负索引读取 |
| 拼接 | concat |
❌ | 新数组 | 合并分页 |
| 裁剪 | slice |
❌ | 片段 | 复制/局部视图 |
| 扁平 | flat/flatMap |
❌ | 新数组 | 降维/展开列表 |
| 归约 | reduce |
❌ | 任意 | 统计/分组/索引化 |
| 变更复制 | toSorted/toReversed/toSpliced/with |
❌ | 新数组 | ES2023 不破坏性替代 |
| 静态 | Array.isArray |
--- | boolean |
类型判定 |
| 静态 | Array.from |
--- | 新数组 | 类数组/迭代转真数组 |
| 静态 | Array.of |
--- | 新数组 | 以实参数组化 |
附录:面试/实战高频 Q&A
Q:为什么 React 里 sort() 会导致 UI "莫名其妙"变动?
A:sort 是就地排序,改了原数组引用 。如果这个数组被多个组件共享或被当作依赖,可能触发不可预期的重渲染/比较失败。用 toSorted 或 arr.slice().sort()。
Q:如何从数组中删除某个元素但不改原数组?
A:toSpliced(i, 1);或 arr.slice(0,i).concat(arr.slice(i+1))。
Q:如何稳定排序多条件?
A:先派生出用于比较的字段(或使用 by/asc/desc 组合器),toSorted 返回新数组,ES2019+ 稳定排序叠加生效。
Q:如何高性能查交集/差集?
A:把较大的那组放到 Set,再 filter 较小那组,时间从 O(n*m) 降到 O(n+m)。
小结
- 选方法先看是否修改原数组 :业务状态推荐纯函数。
- ES2023 的 "toXxx/with" 是提升代码健壮性的关键升级。
- 掌握常见配方(去重/分组/索引化/集合运算/不可变更新),你的业务代码会又短又稳。
- 牢记坑位图 与复杂度,性能与正确性两手抓。