🔥 一文掌握 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
/-1
includes
适合纯等值 (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 -> item
Map)
- 汇总统计、分组、索引化(
-
易错点
- 务必传初始值 ,否则空数组会抛
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" 是提升代码健壮性的关键升级。
- 掌握常见配方(去重/分组/索引化/集合运算/不可变更新),你的业务代码会又短又稳。
- 牢记坑位图 与复杂度,性能与正确性两手抓。