JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南
数组是 JS 里使用频率最高的数据结构,但很多人只停留在
map/filter/forEach三板斧。这篇文章从方法分类 讲到实战配方 ,再到异步并发 和性能避坑 ,一次性讲透。
适合:前端开发、面试复习、代码重构参考。
目录
- 一、先搞懂数组的本质
- 二、方法总览:谁会改原数组
- [三、增删改:9 个会修改原数组的方法](#三、增删改:9 个会修改原数组的方法)
- 四、查找与判断:只读方法
- [五、遍历:forEach / for...of / for / map 怎么选](#五、遍历:forEach / for…of / for / map 怎么选)
- [六、转换:map / filter / slice / flat / flatMap](#六、转换:map / filter / slice / flat / flatMap)
- 七、reduce:数组方法的天花板
- [八、ES2022 ~ ES2024 新 API](#八、ES2022 ~ ES2024 新 API)
- [九、30 个高频实战场景](#九、30 个高频实战场景)
- 十、异步数组操作(面试高频)
- 十一、性能:什么时候别用高阶函数
- [十二、20 个避坑指南](#十二、20 个避坑指南)
- 十三、速查手册
- 十四、总结
一、先搞懂数组的本质
1.1 数组到底是什么
JS 的数组不是 传统意义上的连续内存块,而是一种特殊的对象:
js
const arr = ['a', 'b', 'c']
typeof arr // 'object'
Object.keys(arr) // ['0', '1', '2'] ← 下标其实是字符串 key
arr.length // 3 特殊属性,会自动维护
所以:
- 用
typeof无法 判断数组,必须用Array.isArray() - 数组可以拥有非数字属性(虽然不推荐):
arr.foo = 1 - 数组下标最大
2^32 - 2,超出会变成普通属性
js
Array.isArray([]) // true
Array.isArray(new Set()) // false
Array.isArray({}) // false
Array.isArray('abc') // false ← 字符串不是数组
1.2 创建数组的 7 种方式
js
// 1. 字面量(最常用)
const a = [1, 2, 3]
// 2. new Array(n) ------ ⚠️ 陷阱:单数字参数表示长度,不是元素
new Array(3) // [ , , ] 长度 3 的空数组
new Array(3, 4) // [3, 4] 多个参数才是元素
// 3. Array.of() ------ 解决上面的歧义
Array.of(3) // [3] ✅
Array.of(3, 4) // [3, 4]
// 4. Array.from() ------ 把类数组/可迭代对象转成数组
Array.from('abc') // ['a', 'b', 'c']
Array.from(new Set([1, 1, 2])) // [1, 2]
Array.from({ length: 3 }, (_, i) => i * 2) // [0, 2, 4]
// 5. new Array(n).fill() ------ 创建定长数组
new Array(5).fill(0) // [0, 0, 0, 0, 0]
// 6. 展开运算符(拷贝 / 合并)
const b = [...a] // 浅拷贝
const c = [...a, ...b] // 合并
// 7. 展开 + keys(生成连续数字)
[...Array(5).keys()] // [0, 1, 2, 3, 4]
Array.from({ length: 5 }, (_, i) => i) // 同上,且性能更好
⚠️ 反面教材 :
new Array(5).map(() => 1)得到的是[ , , , , ],因为map会跳过空洞 。想填充必须用fill或Array.from。
1.3 稀疏数组(holes)------ 90% 的人没注意的坑
js
const sparse = [1, , 3] // 中间有个"洞"
const dense = [1, undefined, 3] // 显式的 undefined
sparse.length // 3
1 in sparse // false ← 没有这个索引,是"洞"
1 in dense // true ← 有索引,值是 undefined
// 关键差异:大部分遍历方法会跳过洞!
sparse.map(x => x * 2) // [2, , 6] 长度 3,中间仍是洞
sparse.forEach(x => console.log(x)) // 只打印 1 和 3(只执行 2 次)
dense.forEach(x => console.log(x)) // 打印 1, undefined, 3(执行 3 次)
// 这些操作会把洞变成 undefined
[...sparse] // [1, undefined, 3]
Array.from(sparse) // [1, undefined, 3]
// 这些操作会移除洞
sparse.flat() // [1, 3] 长度变成 2
sparse.filter(() => true) // [1, 3]
什么时候会产生洞? new Array(5)、delete arr[i]、arr.length = 10。
怎么避免? 别用 delete 删元素(用 splice),别用 new Array(n) 直接 map。
二、方法总览:谁会改原数组
这是数组最容易踩坑的地方,先看这张表再写代码。
| 分类 | 方法 |
|---|---|
| ✅ 会改原数组(9 个) | push pop shift unshift splice sort reverse fill copyWithin |
| ❌ 不改(返回新值) | map filter slice concat flat flatMap join reduce toSorted toReversed toSpliced with |
| 🔍 只查找 / 判断 | find findIndex findLast findLastIndex includes indexOf lastIndexOf some every at |
| 🔁 只遍历 | forEach entries keys values |
返回值速记
| 方法 | 找不到 / 空数组时返回 |
|---|---|
find |
undefined |
findIndex / indexOf / lastIndexOf |
-1 |
filter / map / flat / slice |
[] |
some |
false |
every |
true(空真命题) |
reduce(无初始值) |
空数组直接抛 TypeError |
口诀:除了那 9 个「破坏性」方法,其余都返回新数组或新值,原数组安全。
三、增删改:9 个会修改原数组的方法
3.1 push / pop ------ 尾部操作,O(1)
js
const a = [1, 2, 3]
a.push(4) // 返回新长度 4 a → [1, 2, 3, 4]
a.push(5, 6) // 支持多个 a → [1, 2, 3, 4, 5, 6]
a.pop() // 返回被删的 6 a → [1, 2, 3, 4, 5]
a.pop() // 返回被删的 5 a → [1, 2, 3, 4]
// ⚠️ push 返回的是长度,不是数组,不能链式继续调数组方法
a.push(9).map(x => x) // ❌ TypeError: a.push(...).map is not a function
用 push 合并数组的正确姿势:
js
const a = [1], b = [2, 3]
a.push(...b) // ✅ a → [1, 2, 3]
a.push(b) // ❌ a → [1, [2, 3]] 变成了嵌套数组
a.concat(b) // ✅ 返回新数组,不改 a
3.2 shift / unshift ------ 头部操作,O(n)
js
const a = [1, 2, 3]
a.unshift(0) // 返回新长度 4 a → [0, 1, 2, 3]
a.shift() // 返回被删的 0 a → [1, 2, 3]
⚠️ 性能警告 :头部增删需要整体搬移所有元素,复杂度 O(n)。
10 万个元素上循环
shift()会卡死页面。大数据量请改用双端队列 或倒序遍历:
js// 从头部逐个取出的高性能写法 for (let i = 0; i < arr.length; i++) { const item = arr[i] // 用下标访问代替 shift }
3.3 splice ------ 瑞士军刀(删除 / 插入 / 替换)
js
// splice(start, deleteCount, ...items)
// 返回值:被删除元素组成的数组(非常重要!)
const a = [1, 2, 3, 4, 5]
a.splice(1, 2) // 从下标 1 删 2 个 → 返回 [2, 3],a → [1, 4, 5]
a.splice(1, 0, 'x') // 删 0 个、插入 → 返回 [],a → [1, 'x', 4, 5]
a.splice(1, 1, 'y') // 删 1 个、插入 → 返回 ['x'],a → [1, 'y', 4, 5]
// 负索引:从后往前数
const b = [1, 2, 3, 4, 5]
b.splice(-2, 1) // 返回 [4],b → [1, 2, 3, 5]
// 只传 start:删到末尾
const c = [1, 2, 3, 4]
c.splice(2) // 返回 [3, 4],c → [1, 2]
splice 三大经典用法:
js
// ① 删除指定下标
arr.splice(2, 1)
// ② 在指定位置插入
arr.splice(2, 0, 'newItem')
// ③ 用值删除元素(替代 delete)
const idx = arr.indexOf(target)
if (idx > -1) arr.splice(idx, 1)
// ④ 替换元素的快捷写法
arr.splice(idx, 1, newItem)
// ⑤ 移动元素(把 from 移到 to)
arr.splice(to, 0, ...arr.splice(from, 1))
❌ 不要用
delete arr[i]:它只删属性值,不改变length,会留下空洞 ,后续forEach/map都会漏掉它。
jsconst a = [1, 2, 3] delete a[1] console.log(a) // [1, empty, 3] console.log(a.length) // 3 ← 长度没变!
3.4 sort ------ 排序(坑最多)
js
const nums = [10, 9, 100]
nums.sort() // ❌ [10, 100, 9]
为什么? sort() 不传比较函数时,会把每个元素先 String() 转换,再按 UTF-16 码点 排序。'10' < '100' < '9'。
js
nums.sort((a, b) => a - b) // ✅ [9, 10, 100] 升序
nums.sort((a, b) => b - a) // ✅ [100, 10, 9] 降序
6 种实战排序场景:
js
const users = [
{ name: '张三', age: 28, level: '中' },
{ name: '李四', age: 22, level: '高' },
{ name: '王五', age: 28, level: '低' },
]
// ① 按数值字段
users.sort((a, b) => a.age - b.age)
// ② 按字符串字段(中文友好)
users.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
// ③ 多级排序:先按 age 升序,age 相同再按 name
users.sort((a, b) => a.age - b.age || a.name.localeCompare(b.name, 'zh-CN'))
// ④ 按自定义权重(枚举值排序)
const order = ['高', '中', '低']
users.sort((a, b) => order.indexOf(a.level) - order.indexOf(b.level))
// ⑤ 按原数组中的顺序(自定义顺序数组)
const targetOrder = ['李四', '王五', '张三']
users.sort((a, b) => targetOrder.indexOf(a.name) - targetOrder.indexOf(b.name))
// ⑥ 稳定排序(ES2019+ 原生保证)
// 排序前后同值元素的相对顺序不变
洗牌的正确与错误写法:
js
// ❌ 错误:分布不均匀,某些排列永远不会出现
arr.sort(() => Math.random() - 0.5)
// ✅ 正确:Fisher-Yates 洗牌算法
function shuffle(arr) {
const a = arr.slice() // 不修改原数组
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[a[i], a[j]] = [a[j], a[i]]
}
return a
}
不改原数组的排序:
js
const sorted1 = [...arr].sort((a, b) => a - b) // ES6
const sorted2 = arr.toSorted((a, b) => a - b) // ES2023 ✅ 推荐
3.5 reverse ------ 反转
js
const a = [1, 2, 3]
a.reverse() // 返回原数组(已反转),a → [3, 2, 1]
// 不改原数组
const rev1 = [...a].reverse()
const rev2 = a.toReversed() // ES2023
3.6 fill ------ 填充
js
const a = [1, 2, 3, 4, 5]
a.fill(0) // [0, 0, 0, 0, 0] 全部填充
a.fill(9, 1) // [0, 9, 9, 9, 9] 从下标 1 到末尾
a.fill(8, 1, 3) // [0, 8, 8, 9, 9] [1, 3) 区间填充
// 经典用途:初始化矩阵
const matrix = Array.from({ length: 3 }, () => new Array(3).fill(0))
// [[0,0,0], [0,0,0], [0,0,0]]
⚠️
new Array(3).fill([])会让三行共享同一个数组引用 !必须用Array.from或map生成新数组。
3.7 copyWithin ------ 内部复制
js
// copyWithin(target, start, end):把 [start, end) 的内容复制到 target 处
const a = [1, 2, 3, 4, 5]
a.copyWithin(0, 3) // a → [4, 5, 3, 4, 5]
// 实际用途很少,了解即可
3.8 length 的妙用(清空 / 截断)
js
const a = [1, 2, 3, 4, 5]
a.length = 0 // 清空数组(保留引用,其它变量指向的还是同一个数组)
a.length = 3 // 截断到 3 个元素
a.length = 10 // 扩容,多出来的位置是洞
// 与重新赋值的区别
let b = [1, 2, 3]
const ref = b
b = [] // ref 仍是 [1,2,3] ← 重新赋值不影响其它引用
ref.length = 0 // ref 和 b 都变成 [] ← 用 length 才能真正清空所有引用
四、查找与判断:只读方法
4.1 at() ------ 负索引(ES2022)
js
const arr = [10, 20, 30, 40]
arr.at(0) // 10
arr.at(-1) // 40 ← 最后一个,比 arr[arr.length-1] 香太多
arr.at(-2) // 30
// 对比旧写法
arr[arr.length - 1] // 40 啰嗦
4.2 includes vs indexOf ------ NaN 的坑
js
const arr = [1, 'a', NaN, null]
arr.includes('a') // true
arr.includes(NaN) // true ✅
arr.indexOf(NaN) // -1 ❌ 找不到!
arr.indexOf('a') // 1
arr.indexOf(1) // 0 找到返回下标
arr.indexOf('x') // -1 找不到返回 -1
为什么? indexOf 用 严格相等 === 比较,而 NaN === NaN 是 false;includes 用 SameValueZero 算法,能匹配 NaN。
| 场景 | indexOf |
includes |
|---|---|---|
找 NaN |
❌ -1 |
✅ true |
找 undefined(洞) |
❌ -1 |
✅ true |
| 需要下标 | ✅ | ❌ 不支持 |
| 语义清晰度 | 一般 | 更好 |
结论 :只判断「有没有」用
includes;需要下标用indexOf/findIndex。
js
// 判断数组是否包含对象的某个值
const users = [{ id: 1 }, { id: 2 }]
users.includes({ id: 1 }) // ❌ false(引用类型比的是地址)
users.some(u => u.id === 1) // ✅ true
users.findIndex(u => u.id === 1) // ✅ 0
4.3 find / findIndex / findLast / findLastIndex
js
const users = [
{ id: 1, name: '张三', vip: false },
{ id: 2, name: '李四', vip: true },
{ id: 3, name: '王五', vip: true },
]
// 正向查找(命中即停,性能优于 filter)
users.find(u => u.vip) // { id: 2, ... }
users.findIndex(u => u.vip) // 1
users.find(u => u.id === 99) // undefined
users.findIndex(u => u.id === 99) // -1
// 反向查找(ES2023)
users.findLast(u => u.vip) // { id: 3, ... }
users.findLastIndex(u => u.vip) // 2
find vs filter 性能对比:
js
const bigArr = Array.from({ length: 100000 }, (_, i) => ({ id: i }))
// 找一个元素
bigArr.find(x => x.id === 1) // 第 2 次就命中并停止 ✅
bigArr.filter(x => x.id === 1) // 遍历全部 10 万次 ❌ 只为了取 [0]
bigArr.filter(x => x.id === 1)[0] // 更糟 ❌
4.4 some / every ------ 真假判断
js
const nums = [2, 4, 6, 8]
nums.every(n => n % 2 === 0) // true 全部满足
nums.some(n => n > 6) // true 至少一个满足
nums.some(n => n > 10) // false
nums.every(n => n > 10) // false
// ⚠️ 空数组的"空真命题"
[].every(n => n > 0) // true ← 容易误判!
[].some(n => n > 0) // false
⚠️ 空数组
every返回true是数学上的「空真命题」,但业务上常常是 bug 来源。做校验前先判空:
jsconst isValid = list.length > 0 && list.every(check)
短路特性(性能优化点):
js
// some 命中第一个 true 就停止
[1, 2, 3, 4, 5].some(n => {
console.log(n) // 只打印 1, 2, 3
return n === 3
})
// every 命中第一个 false 就停止
[1, 2, 3, 4, 5].every(n => {
console.log(n) // 只打印 1, 2, 3
return n < 4
})
实际用途:
js
// 表单校验
const canSubmit = fields.length > 0 && fields.every(f => f.value.trim() !== '')
// 权限判断
const hasPermission = roles.some(r => ['admin', 'owner'].includes(r))
// 判断是否存在重复
const hasDuplicate = arr.some((v, i) => arr.indexOf(v) !== i)
4.5 查找的三个衍生问题
js
const arr = [1, 2, 3, 2, 1]
// ① 所有匹配项的**下标**
const indices = arr.reduce((acc, v, i) => (v === 2 && acc.push(i), acc), []) // [1, 3]
// 现代写法
const indices2 = arr.map((v, i) => (v === 2 ? i : -1)).filter(i => i !== -1) // [1, 3]
// ② 统计出现次数
const count = arr.filter(v => v === 2).length // 2
// ③ 是否存在重复值
const dup = arr.some((v, i) => arr.indexOf(v) !== i) // true
五、遍历:forEach / for...of / for / map 怎么选
5.1 四种遍历方式对比
| 特性 | for |
for...of |
forEach |
map |
|---|---|---|---|---|
能否 break / continue |
✅ | ✅ | ❌ | ❌ |
能否 await |
✅ | ✅ | ❌ 语义错误 | ❌ 语义错误 |
能否 return 跳出函数 |
✅ | ✅ | ❌ 只能跳回调 | ❌ |
| 是否返回新数组 | ❌ | ❌ | ❌(undefined) |
✅ |
| 能否跳过空洞 | ❌ 会遍历 | ❌ 会遍历 | ✅ 跳过 | ✅ 跳过 |
| 性能 | 最快 | 较快 | 中等 | 中等 |
| 可读性 | 一般 | 好 | 好 | 好 |
5.2 forEach 的三个致命限制
js
const arr = [1, 2, 3, 4]
// ① 不能中断
arr.forEach(n => {
if (n === 3) return // ❌ 只是跳过本次回调,后续还会继续执行
console.log(n) // 1, 2, 4
})
// 想中断只能抛异常(非常丑陋)
try {
arr.forEach(n => {
if (n === 3) throw new Error('break')
console.log(n)
})
} catch (e) { /* ... */ }
// ② 不能 await
arr.forEach(async n => {
await doSomething(n) // ❌ 外层不会等待,等于并发且不可控
})
// 正确做法:
await Promise.all(arr.map(n => doSomething(n))) // 并发
for (const n of arr) { await doSomething(n) } // 串行
// ③ 返回 undefined
const result = arr.forEach(n => n * 2)
console.log(result) // undefined ❌ 想要结果必须用 map
5.3 选择建议
js
// 只是执行副作用(打印、写 DOM、上报)→ forEach
list.forEach(item => track(item))
// 需要中断 / 需要 await / 逻辑复杂 → for...of
for (const item of list) {
if (item.done) break
await save(item)
}
// 需要"变形"出新数组 → map
const names = list.map(i => i.name)
// 需要"筛选" → filter
const actives = list.filter(i => i.active)
// 极致性能 + 大数组 → for
for (let i = 0; i < list.length; i++) {
total += list[i]
}
5.4 for...of 与 for...in 的区别
js
const arr = ['a', 'b', 'c']
arr.extra = 'x' // 给数组加了个自定义属性
// for...of:遍历**值**(推荐,只遍历索引元素)
for (const v of arr) console.log(v) // 'a', 'b', 'c'
// for...in:遍历**键**(包括自定义属性,且是字符串,不推荐用于数组)
for (const k in arr) console.log(k) // '0', '1', '2', 'extra' ❌
结论 :数组遍历永远用
for...of,for...in留给对象。
六、转换:map / filter / slice / flat / flatMap
6.1 map ------ 1:1 映射
js
// 基本用法:长度恒定不变
[1, 2, 3].map(n => n * 2) // [2, 4, 6]
[1, 2, 3].map((n, i) => `${i}:${n}`) // ['0:1', '1:2', '2:3']
// 对象数组提取字段
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]
users.map(u => u.name) // ['张三', '李四']
users.map(u => ({ ...u, label: u.name })) // 返回对象需用 () 包裹,否则被当成函数体
⚠️ 经典坑 :
map(u => { name: u.name })会返回undefined,因为{}被解析成了函数体而非对象字面量。
map 的 4 个高阶用法:
js
// ① 提取 + 重命名
users.map(({ id, name: userName }) => ({ id, userName }))
// ② 生成序号
users.map((u, i) => ({ ...u, index: i + 1 }))
// ③ 条件映射
users.map(u => u.age >= 18 ? '成年' : '未成年')
// ④ 调用方法收集结果
users.map(u => u.getName())
6.2 filter ------ 条件筛选
js
[1, 2, 3, 4, 5].filter(n => n % 2 === 1) // [1, 3, 5]
// 去伪值(最常用!)
const dirty = [0, 1, null, 2, undefined, 3, '', NaN, false, 4]
dirty.filter(Boolean) // [1, 2, 3, 4]
// 对象数组筛选
users.filter(u => u.age > 18)
// 去重(利用 indexOf)
arr.filter((v, i, a) => a.indexOf(v) === i)
// 对象数组按 key 去重
users.filter((u, i, a) => a.findIndex(x => x.id === u.id) === i)
6.3 slice ------ 截取(不改原数组)
js
const arr = [1, 2, 3, 4, 5]
arr.slice(1, 3) // [2, 3] 不含 end
arr.slice(2) // [3, 4, 5] 从 2 到末尾
arr.slice(-2) // [4, 5] 最后两个
arr.slice(0, -1) // [1, 2, 3, 4] 去掉最后一个
arr.slice() // [1,2,3,4,5] 浅拷贝
// 经典用途:浅拷贝
const copy = arr.slice()
// 经典用途:只取前 n 个
const topN = arr.slice(0, 10)
⚠️
slice和splice千万别搞混:
slice:截取,不改原数组,返回截取的部分splice:改 原数组,返回被删掉的部分
jsconst a = [1, 2, 3] a.slice(0, 2) // 返回 [1, 2],a 仍是 [1, 2, 3] a.splice(0, 2) // 返回 [1, 2],a 变成 [3]
6.4 flat / flatMap ------ 拍平
js
// flat(depth):按深度拍平,默认 1 层
[1, [2, 3], [4, [5, [6]]]].flat() // [1, 2, 3, 4, [5, [6]]]
[1, [2, 3], [4, [5, [6]]]].flat(2) // [1, 2, 3, 4, 5, [6]]
[1, [2, 3], [4, [5, [6]]]].flat(Infinity) // [1, 2, 3, 4, 5, 6]
// 顺带清理空洞
[1, , 3].flat() // [1, 3]
// flatMap:map + flat(1),只能拍 1 层,但性能更好(少一次遍历)
['hello world', 'foo bar'].flatMap(s => s.split(' '))
// ['hello', 'world', 'foo', 'bar']
[1, 2, 3].flatMap(n => n === 2 ? [] : [n]) // [1, 3] 顺便实现"条件过滤 + 映射"
flatMap比map().flat()好在哪:少创建一次中间数组,且只需遍历一遍。
6.5 concat / 展开运算符 ------ 合并
js
const a = [1], b = [2, 3]
// concat:返回新数组,支持混合值
a.concat(b) // [1, 2, 3]
a.concat(b, 4, [5]) // [1, 2, 3, 4, 5]
// 展开运算符:语法更简洁(推荐)
[...a, ...b] // [1, 2, 3]
[...a, 4, ...[5]] // [1, 4, 5]
// ⚠️ concat 会把数组拍平 1 层,展开运算符不会
[1].concat([2, [3]]) // [1, 2, [3]]
[1, [2, [3]]] // 手动写就是嵌套
[...[1], ...[2, [3]]] // [1, 2, [3]]
6.6 join / toString ------ 转字符串
js
const arr = [1, null, undefined, 2, 3]
arr.join() // '1,,,2,3' 默认逗号,null/undefined 变空字符串
arr.join('-') // '1---2-3'
arr.join('') // '123'
arr.toString() // '1,,,2,3' 等同于 join()
// 实用技巧:拼接 SQL 占位符、class 名
const ids = [1, 2, 3]
`WHERE id IN (${ids.join(',')})` // 'WHERE id IN (1,2,3)'
const classes = ['btn', 'btn-primary', active && 'active'].filter(Boolean).join(' ')
6.7 keys / values / entries ------ 迭代器
js
const arr = ['a', 'b', 'c']
// 转成数组
[...arr.keys()] // [0, 1, 2] 下标
[...arr.values()] // ['a', 'b', 'c'] 值
[...arr.entries()] // [[0,'a'], [1,'b'], [2,'c']]
// 边遍历边拿下标(比 forEach 更灵活,可 break)
for (const [i, v] of arr.entries()) {
if (v === 'b') break
console.log(i, v) // 0 'a'
}
// Array.from 转换
Array.from(arr.entries()) // [[0,'a'], [1,'b'], [2,'c']]
七、reduce:数组方法的天花板
7.1 参数与执行流程
js
arr.reduce((accumulator, currentValue, currentIndex, array) => {
// 返回值会作为下一轮的 accumulator
return newAccumulator
}, initialValue)
逐轮执行图解:
js
[1, 2, 3, 4].reduce((acc, cur, idx) => {
console.log(`第 ${idx} 轮: acc=${acc}, cur=${cur}`)
return acc + cur
}, 0)
// 第 0 轮: acc=0, cur=1
// 第 1 轮: acc=1, cur=2
// 第 2 轮: acc=3, cur=3
// 第 3 轮: acc=6, cur=4
// 最终返回: 10
不传初始值时:
js
[1, 2, 3, 4].reduce((acc, cur, idx) => {
console.log(`第 ${idx} 轮: acc=${acc}, cur=${cur}`)
return acc + cur
})
// 第 1 轮: acc=1, cur=2 ← 注意从 idx=1 开始!
// 第 2 轮: acc=3, cur=3
// 第 3 轮: acc=6, cur=4
// 返回 10
7.2 初始值陷阱
js
[].reduce((a, b) => a + b)
// ❌ TypeError: Reduce of empty array with no initial value
[].reduce((a, b) => a + b, 0)
// ✅ 0
[{ price: 10 }].reduce((a, b) => a + b.price)
// ✅ 10(没有初始值时,取第 0 项当累加器)
[{ price: 10 }].reduce((a, b) => a + b.price, 0)
// ❌ NaN(0 是数字,0 + 对象的 price 逻辑不通)
// 正确写法:初始化成对象或数字
[{ price: 10 }].reduce((sum, b) => sum + b.price, 0) // ✅ 10
金律 :永远显式传初始值,且初始值的类型要和累加器最终类型一致。
7.3 13 个实战场景
js
const nums = [1, 2, 3, 4, 5]
const users = [
{ id: 1, name: '张三', dept: '研发', age: 28 },
{ id: 2, name: '李四', dept: '研发', age: 32 },
{ id: 3, name: '王五', dept: '市场', age: 25 },
]
// ① 求和
nums.reduce((sum, n) => sum + n, 0) // 15
// ② 求最值(安全版,不怕爆栈)
nums.reduce((max, n) => (n > max ? n : max), -Infinity) // 5
users.reduce((oldest, u) => (u.age > oldest.age ? u : oldest)) // 李四
// ③ 求平均
const sum = nums.reduce((s, n) => s + n, 0)
const avg = nums.length ? sum / nums.length : 0 // 3
// ④ 计数 / 词频统计
const words = ['a', 'b', 'a', 'c', 'a']
words.reduce((acc, w) => ((acc[w] = (acc[w] || 0) + 1), acc), {})
// { a: 3, b: 1, c: 1 }
// ⑤ 数组 → 对象(建索引,超常用)
users.reduce((acc, u) => { acc[u.id] = u; return acc }, {})
// { '1': {...}, '2': {...}, '3': {...} }
// 或用 Object.fromEntries(更快)
Object.fromEntries(users.map(u => [u.id, u]))
// ⑥ 分组
users.reduce((acc, u) => {
(acc[u.dept] ||= []).push(u)
return acc
}, {})
// { 研发: [张三, 李四], 市场: [王五] }
// ⑦ 去重
nums.concat([3, 4]).reduce((acc, n) => acc.includes(n) ? acc : [...acc, n], [])
// ⑧ 二维拍平
[[1, 2], [3, 4]].reduce((acc, cur) => acc.concat(cur), [])
// 现代写法:[[1,2],[3,4]].flat()
// ⑨ 对象数组按字段求和
users.reduce((sum, u) => sum + u.age, 0) // 85
// ⑩ 数组 → 字符串拼接
users.reduce((str, u, i) => str + (i ? ', ' : '') + u.name, '')
// ⑪ 查找替换(把符合条件的元素整体替换)
users.reduce((acc, u) => {
acc.push(u.id === 2 ? { ...u, age: 33 } : u)
return acc
}, [])
// 更简洁:users.map(u => u.id === 2 ? { ...u, age: 33 } : u)
// ⑫ Promise 串行执行(reduce 的杀手锏)
await tasks.reduce(
(promise, task) => promise.then(() => runTask(task)),
Promise.resolve()
)
// ⑬ 按顺序执行中间件(洋葱模型,Koa 就是这么实现的)
7.4 关于 reduce 的两点提醒
- 可读性 :
reduce很强大,但过度使用会变成「密码」。如果一个reduce需要写注释才能看懂,for循环可能是更好的选择。 - 性能 :
reduce因为有闭包调用和每轮的返回值传递,通常比for慢 1.5 ~ 3 倍 。1000 万次求和实测:for约 15ms,reduce约 45ms。但业务代码里这点差距通常不重要。
八、ES2022 ~ ES2024 新 API
8.1 Array.prototype.at(ES2022)
js
const arr = [10, 20, 30]
arr.at(-1) // 30
arr.at(-2) // 20
8.2 findLast / findLastIndex(ES2023)
js
const arr = [1, 2, 3, 4, 5]
arr.findLast(n => n < 4) // 3
arr.findLastIndex(n => n < 4) // 2
8.3 不可变方法:toSorted / toReversed / toSpliced / with(ES2023)
这是 ES2023 对数组最重要的补强 ------终于有了不改原数组的 sort / reverse / splice / 赋值。
js
const arr = [3, 1, 2]
arr.toSorted((a, b) => a - b) // [1, 2, 3] arr 不变
arr.toReversed() // [2, 1, 3] arr 不变
arr.toSpliced(1, 1, 'x') // [3, 'x', 2] arr 不变
arr.with(0, 99) // [99, 1, 2] arr 不变
新旧对比:
| 可变(改原数组) | 不可变(返回新数组) |
|---|---|
arr.sort(cmp) |
arr.toSorted(cmp) |
arr.reverse() |
arr.toReversed() |
arr.splice(...) |
arr.toSpliced(...) |
arr[i] = v |
arr.with(i, v) |
为什么重要:在 Vue / React 这类依赖「引用变化」触发更新的框架里,不可变方法能让数据流更可预测:
js
// Vue 3 中
const list = ref([3, 1, 2])
// 写法 A:原地修改(Vue 能追踪到,但语义上"偷偷改了")
list.value.sort((a, b) => a - b)
// 写法 B:不可变替换(语义清晰,副作用明确)
list.value = list.value.toSorted((a, b) => a - b)
兼容性 :Chrome 110+ / Edge 110+ / Safari 16.4+ / Firefox 115+(2023 年 3 月起)。老项目需要
core-jspolyfill 或使用browserslist配置转译。
8.4 Object.groupBy / Map.groupBy(ES2024)
以前分组只能写 reduce,现在有官方 API:
js
const users = [
{ name: '张三', dept: '研发' },
{ name: '李四', dept: '研发' },
{ name: '王五', dept: '市场' },
]
// 以前(reduce 手写)
users.reduce((acc, u) => ((acc[u.dept] ||= []).push(u), acc), {})
// 现在(ES2024)✅
Object.groupBy(users, u => u.dept)
// { 研发: [张三, 李四], 市场: [王五] }
// ⚠️ 返回的是 null 原型对象,不是普通 Object
历史小插曲 :这个 API 最初叫
Array.prototype.group,因为「不能给Object.prototype之外的原型污染」等争议,最终改名并挪到了Object上。兼容性:Chrome 117+ / Safari 17.4+ / Firefox 119+。
如果需要 Map 而不是对象(key 可以是任意类型),用 Map.groupBy:
js
const grouped = Map.groupBy(users, u => u.dept)
grouped.get('研发') // [张三, 李四]
8.5 Array.fromAsync(ES2025)
把异步可迭代对象转成数组:
js
const arr = await Array.fromAsync(asyncGenerator())
const arr2 = await Array.fromAsync(
{ length: 3 },
async (_, i) => await fetchItem(i)
)
兼容性较新(Chrome 121+),生产环境慎用。
九、30 个高频实战场景
9.1 去重与集合运算
js
// ① 基础类型去重
const unique = arr => [...new Set(arr)]
// ② 对象数组按字段去重(保留首次出现)
const uniqueBy = (arr, key) =>
arr.filter((item, i, a) => a.findIndex(x => x[key] === item[key]) === i)
// ③ 对象数组按字段去重(保留最后一次出现)
const uniqueByLast = (arr, key) =>
arr.filter((item, i, a) => a.findLastIndex(x => x[key] === item[key]) === i)
// ④ 交集(都有的)
const intersect = (a, b) => a.filter(x => b.includes(x))
const intersectBy = (a, b, key) => a.filter(x => b.some(y => y[key] === x[key]))
// ⑤ 并集(合并去重)
const union = (a, b) => [...new Set([...a, ...b])]
// ⑥ 差集(a 有 b 没有)
const difference = (a, b) => a.filter(x => !b.includes(x))
// ⑦ 对称差集(只在其中一个里出现)
const symmetricDiff = (a, b) => [
...a.filter(x => !b.includes(x)),
...b.filter(x => !a.includes(x)),
]
9.2 统计计算
js
// ⑧ 求和
const sum = arr => arr.reduce((s, n) => s + n, 0)
// ⑨ 平均值(注意空数组)
const avg = arr => arr.length ? sum(arr) / arr.length : 0
// ⑩ 最大 / 最小值(安全版)
const max = arr => arr.reduce((m, n) => (n > m ? n : m), -Infinity)
const min = arr => arr.reduce((m, n) => (n < m ? n : m), Infinity)
// ⑪ 对象数组按字段求和
const sumBy = (arr, key) => arr.reduce((s, o) => s + o[key], 0)
// ⑫ 字节大小格式化(求和后换算)
const formatSize = bytes => {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let i = 0
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++ }
return `${bytes.toFixed(2)}${units[i]}`
}
9.3 分组与转换
js
// ⑬ 分组(ES2024 原生)
Object.groupBy(users, u => u.dept)
// ⑭ 分组(兼容写法)
const groupBy = (arr, keyFn) =>
arr.reduce((acc, item) => {
const key = keyFn(item)
;(acc[key] ||= []).push(item)
return acc
}, {})
// ⑮ 数组转对象(按 id 建索引)
const toMap = (arr, key) => Object.fromEntries(arr.map(o => [o[key], o]))
// ⑯ 对象转数组(键值对)
Object.entries({ a: 1, b: 2 }) // [['a', 1], ['b', 2]]
// ⑰ 提取字段组成新对象
const pick = (obj, keys) =>
keys.reduce((acc, k) => (k in obj && (acc[k] = obj[k]), acc), {})
// ⑱ 排除字段
const omit = (obj, keys) =>
Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k)))
// ⑲ 数组转 Map(便于 O(1) 查找)
const map = new Map(users.map(u => [u.id, u]))
map.get(2) // 查 id=2 的用户,比 find 快得多
9.4 切分与重组
js
// ⑳ 分块(每 n 个一组)
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
)
chunk([1,2,3,4,5,6,7], 3) // [[1,2,3], [4,5,6], [7]]
// ㉑ 交换元素
;[arr[i], arr[j]] = [arr[j], arr[i]]
// ㉒ 移动元素(把 from 移到 to)
const move = (arr, from, to) => {
const copy = [...arr]
copy.splice(to, 0, ...copy.splice(from, 1))
return copy
}
// ㉓ 旋转(正数右移,负数左移)
const rotate = (arr, n) => {
const k = ((n % arr.length) + arr.length) % arr.length
return [...arr.slice(-k), ...arr.slice(0, -k)]
}
rotate([1,2,3,4,5], 2) // [4, 5, 1, 2, 3]
// ㉔ 随机取一个
const sample = arr => arr[Math.floor(Math.random() * arr.length)]
// ㉕ 随机取 n 个(不重复)
const sampleN = (arr, n) => shuffle(arr).slice(0, n)
9.5 删除与更新
js
// ㉖ 按值删除
const removeValue = (arr, val) => {
const i = arr.indexOf(val)
if (i > -1) arr.splice(i, 1)
return arr
}
// ㉗ 按条件批量删除(不可变写法,推荐)
const removeWhere = (arr, pred) => arr.filter(x => !pred(x))
// ㉘ 按 id 删除对象
const removeById = (arr, id) => arr.filter(x => x.id !== id)
// ㉙ 按 id 更新对象(不可变写法)
const updateById = (arr, id, patch) =>
arr.map(x => (x.id === id ? { ...x, ...patch } : x))
// ㉚ 数组末尾补/截到指定长度
const resize = (arr, len, fill = null) =>
arr.length >= len ? arr.slice(0, len) : [...arr, ...new Array(len - arr.length).fill(fill)]
9.6 其他实用工具
js
// 判断两个数组是否相等(浅比较)
const isEqual = (a, b) =>
a.length === b.length && a.every((v, i) => v === b[i])
// 找出两个数组的差异详情
const diffDetail = (oldArr, newArr) => ({
added: newArr.filter(x => !oldArr.includes(x)),
removed: oldArr.filter(x => !newArr.includes(x)),
kept: oldArr.filter(x => newArr.includes(x)),
})
// 扁平化树形结构
const flattenTree = (tree, childrenKey = 'children') =>
tree.reduce(
(acc, node) => acc.concat(
node,
node[childrenKey] ? flattenTree(node[childrenKey], childrenKey) : []
),
[]
)
// 在树中查找节点路径(面包屑)
const findPath = (tree, pred, path = []) => {
for (const node of tree) {
const cur = [...path, node]
if (pred(node)) return cur
if (node.children) {
const found = findPath(node.children, pred, cur)
if (found) return found
}
}
return null
}
十、异步数组操作(面试高频)
10.1 错误写法:forEach + async
js
const ids = [1, 2, 3]
// ❌ 完全错误的写法
const results = []
ids.forEach(async id => {
const data = await fetchData(id)
results.push(data) // 不确定何时执行完
})
console.log(results) // [] 或残缺数组
问题 :forEach 丢弃回调返回值,async 回调返回的 Promise 没人等,且抛错会变成 unhandledRejection。
10.2 正确写法:map + Promise.all(并发)
js
// ✅ 并发执行,全部完成后拿到结果,且顺序与 ids 一致
const results = await Promise.all(ids.map(id => fetchData(id)))
执行时序:
① map 同步遍历(一个 tick 内完成)
→ fetchData(1) 发起请求 ┐
→ fetchData(2) 发起请求 ├─ 三个请求同时在飞
→ fetchData(3) 发起请求 ┘
← 立即返回 [Promise1, Promise2, Promise3]
② await Promise.all
→ 等最后一个落地
→ 按【原数组顺序】组装结果(不是完成顺序!)
为什么 map 能实现并发? 因为 async 函数被调用时会立刻返回一个 Promise (执行到第一个 await 就交出控制权),而不是阻塞等待。
性能对比(10 个请求,每个耗时 200ms):
| 写法 | 总耗时 |
|---|---|
map + Promise.all |
≈ 200ms(并发) |
for...of + await |
≈ 2000ms(串行) |
10.3 允许部分失败:Promise.allSettled
Promise.all 有个致命特性:只要有一个 Promise reject,整体立刻 reject,其它已完成的结果全部丢弃。
js
// 常见做法一:让子任务自己 try/catch,返回 null,外层过滤
async function fetchSafe(id) {
try {
return await fetchData(id)
} catch (e) {
console.error(`加载 ${id} 失败:`, e)
return null
}
}
const results = (await Promise.all(ids.map(fetchSafe))).filter(Boolean)
js
// 常见做法二:用 allSettled(能拿到失败详情)
const settled = await Promise.allSettled(ids.map(id => fetchData(id)))
const ok = settled
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
const failed = settled
.filter(r => r.status === 'rejected')
.map(r => r.reason)
console.log(`${ok.length} 成功,${failed.length} 失败`)
| 方法 | 行为 |
|---|---|
Promise.all |
一败俱败,全部成功才返回结果 |
Promise.allSettled |
等全部结束,返回 {status, value/reason} 数组 |
Promise.race |
返回最先结束的那个(成功或失败) |
Promise.any |
返回最先成功的那个,全失败才 reject |
10.4 串行执行(后一个依赖前一个)
js
// 方式一:for...of(最直观,推荐)
const results = []
for (const id of ids) {
results.push(await fetchData(id)) // 必须等上一个完成
}
// 方式二:reduce(函数式)
const results = await ids.reduce(
(promise, id) => promise.then(async acc => {
acc.push(await fetchData(id))
return acc
}),
Promise.resolve([])
)
10.5 限流并发(最多同时 N 个)
Promise.all 会一次性打出全部请求。如果数组有几百项,会:
- 触发浏览器同域名并发上限(HTTP/1.1 约 6 个,HTTP/2 更宽但服务端仍有限制)
- 打爆后端,触发限流
js
// 手写限流器(不依赖第三方库)
async function limitRun(items, limit, fn) {
const results = []
const executing = new Set()
for (const [index, item] of items.entries()) {
const p = Promise.resolve()
.then(() => fn(item, index))
.finally(() => executing.delete(p))
results[index] = p // 保序
executing.add(p)
if (executing.size >= limit) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
// 使用:最多 5 个并发
const data = await limitRun(ids, 5, id => fetchData(id))
或者用成熟的库:
js
import pLimit from 'p-limit'
const limit = pLimit(5)
const data = await Promise.all(ids.map(id => limit(() => fetchData(id))))
10.6 for await...of ------ 消费异步迭代器
js
async function* asyncGenerator() {
for (let i = 0; i < 3; i++) {
yield await fetchData(i)
}
}
// 串行消费
for await (const data of asyncGenerator()) {
console.log(data)
}
// 转成数组(ES2025)
const all = await Array.fromAsync(asyncGenerator())
10.7 一个真实案例
需求:从设备树里挑出「地图上已配置坐标」的分组,并发拉取每组的摄像头列表,失败的跳过。
js
async function loadCameraList() {
const { data: groups } = await getDeviceTree()
if (!groups?.length) return []
// ① 过滤:只保留地图上有坐标配置的分组
const validGroups = groups.filter(({ name }) => positionMap[name + '点位'])
// ② 并发:每个分组拉一次列表
const results = await Promise.all(
validGroups.map(({ name, id }) => loadOneCamera(name, id))
)
// ③ 清理:剔除返回 null 的失败项
return results.filter(Boolean)
}
// 单个分组的加载逻辑:自己消化异常,保证不影响其它分组
async function loadOneCamera(name, groupId) {
try {
const { res: list } = await getVideoList({ pageNum: 1, pageSize: 10, ownerCode: groupId })
if (!list?.length) return null
return { name: name + '点位', data: { channelList: list } }
} catch (err) {
console.error(`加载摄像头 ${name} 失败:`, err)
return null
}
}
这段代码的三个要点:
filter做白名单预筛,避免无意义的网络请求map把「分组数组」变成「Promise 数组」,实现并发loadOneCamera内部try/catch+ 外层filter(Boolean),是Promise.all的标准容错配套
可优化点 :如果分组数量很大,
Promise.all会一次打满网络。建议改造成limitRun(validGroups, 5, ...)限流;另外name + '点位'这种字符串拼接做 key 很脆弱,建议在过滤阶段收集未匹配项并console.warn,方便排查配置遗漏。
十一、性能:什么时候别用高阶函数
11.1 基准数据参考
在 1000 万次「求和」的场景下(Chrome / V8,仅作数量级参考):
| 写法 | 耗时(约) |
|---|---|
for (let i...) |
15 ms |
for...of |
120 ms |
reduce |
45 ms |
forEach |
40 ms |
结论:
map/filter/reduce不比for快 。它们牺牲了少量性能,换来可读性、不可变性和顺序保证。
11.2 什么时候该用 for
js
// ① 超大数组的密集计算(10 万条以上)
// ② 循环体内逻辑复杂、需要多处 break / continue
// ③ 需要提前终止且不想引入异常
// ④ 追求极致内存(避免中间数组)
11.3 减少中间数组
js
// ❌ 产生 3 个中间数组
const result = list
.map(x => x.value)
.filter(v => v > 0)
.map(v => v * 2)
// ✅ 合并成一次遍历
const result = []
for (const x of list) {
if (x.value > 0) result.push(x.value * 2)
}
// ✅ 或用 flatMap 合并 map + filter
const result = list.flatMap(x => (x.value > 0 ? [x.value * 2] : []))
11.4 用 Map 代替 find
js
// ❌ O(n²):1000 条数据要比较 100 万次
const result = listA.map(a => listB.find(b => b.id === a.id))
// ✅ O(n):先建索引,再查
const mapB = new Map(listB.map(b => [b.id, b]))
const result = listA.map(a => mapB.get(a.id))
十二、20 个避坑指南
| # | 坑 | 说明 | 正确做法 |
|---|---|---|---|
| 1 | sort() 默认字符串排序 |
[10,9,100].sort() → [10,100,9] |
永远传比较函数 (a,b) => a-b |
| 2 | reduce 忘传初始值 |
空数组直接 TypeError |
永远显式传初始值 |
| 3 | indexOf 找不到 NaN |
用 === 比较 |
用 includes |
| 4 | new Array(n).map() 无效 |
map 会跳过空洞 |
Array.from({length:n}, fn) |
| 5 | forEach + async |
收不到结果、异常丢失 | map + Promise.all |
| 6 | map(async ...) 忘 Promise.all |
拿到的是 Promise 数组 | 必须包 Promise.all |
| 7 | slice / splice 搞混 |
splice 改原数组且返回被删元素 |
要新数组用 slice |
| 8 | sort / reverse 破坏原数组 |
影响其它引用该数组的逻辑 | [...arr].sort() / toSorted() |
| 9 | 浅拷贝后改嵌套对象 | 嵌套对象仍共享引用 | structuredClone 深拷贝 |
| 10 | push 返回长度不是数组 |
无法链式调用 | 分开写 |
| 11 | delete arr[i] |
留下空洞,length 不变 |
用 splice |
| 12 | [] .every() 返回 true |
空真命题,校验会失效 | 先 list.length > 0 |
| 13 | includes 比对象失效 |
引用类型比的是地址 | 用 some / findIndex |
| 14 | map(u => { name: u.name }) |
{} 被当函数体 |
map(u => ({ name: u.name })) |
| 15 | new Array(3).fill([]) |
三行共享同一数组 | Array.from({length:3}, () => []) |
| 16 | arr.length 在遍历中被改 |
遍历范围已锁定,行为诡异 | 遍历前先确定长度 |
| 17 | 稀疏数组遍历次数不对 | forEach / map 跳过空洞 |
用 Array.from 规整 |
| 18 | Vue 中直接改 props 数组 |
违反单向数据流 | props.list.slice().sort() |
| 19 | sort 是不稳定排序 |
ES2019 前不保证同值顺序 | 现代环境已稳定,老环境需加 tie-breaker |
| 20 | 用 == 判断数组相等 |
引用比较,永远 false | JSON.stringify 或逐项比较 |
关于第 18 条(Vue 场景)展开
js
// ❌ 直接改 props 数组,父组件数据被"偷改"
props.list.sort((a, b) => a.id - b.id)
// ✅ 拷贝后再操作
const sorted = [...props.list].sort((a, b) => a.id - b.id)
// 或
const sorted = props.list.toSorted((a, b) => a.id - b.id)
js
// ref 数组的响应式陷阱
const list = ref([1, 2, 3])
// ✅ 这些能被 Vue 追踪(响应式数组方法被改写)
list.value.push(4)
list.value.sort()
list.value.splice(0, 1)
// ⚠️ 通过下标赋值在老版本 Vue 2 中不触发更新(Vue 3 Proxy 已修复)
list.value[0] = 99
// ⚠️ 整体替换数组时不要用 length = 0
list.value.length = 0 // Vue 3 能追踪,但语义不直观
list.value = [] // ✅ 推荐,语义清晰
十三、速查手册
13.1 决策表:我要做什么
| 需求 | 用什么 |
|---|---|
| 每个元素变个样,数量不变 | map |
| 挑出符合条件的 | filter |
| 找一个元素 / 它的下标 | find / findIndex |
| 从后往前找 | findLast / findLastIndex |
| 判断有没有 / 是不是全都 | some / every |
| 算出一个值(和、最大、分组) | reduce |
| 拍平嵌套数组 | flat / flatMap |
| 截取一段 | slice |
| 删除 / 插入 / 替换元素 | splice(改原数组)/ toSpliced(不改) |
| 排序 | sort(cmp) / toSorted(cmp) |
| 反转 | reverse() / toReversed() |
| 去重 | [...new Set(arr)] |
| 只要执行副作用 | forEach |
需要 break / await |
for...of |
| 判断是不是数组 | Array.isArray() |
| 并发请求一批数据 | map + Promise.all |
| 并发但允许失败 | map + Promise.allSettled |
| 串行请求 | for...of + await |
| 限流并发 | p-limit 或手写 limitRun |
| 数组转 Map 加速查找 | new Map(arr.map(x => [x.id, x])) |
| 按 key 分组 | Object.groupBy(arr, fn)(ES2024) |
13.2 一页纸记忆卡
js
// ===== 会改原数组的 9 个 =====
push pop shift unshift splice sort reverse fill copyWithin
// ===== 返回值速查 =====
find → undefined
findIndex → -1
indexOf → -1
filter/map → []
some → false
every → true(空数组也 true!)
reduce → 空数组无初始值会抛错
// ===== 三个"不改原数组"的 ES2023 新方法 =====
toSorted toReversed toSpliced (+ with)
// ===== 异步三件套 =====
await Promise.all(arr.map(fn)) // 并发
await Promise.allSettled(arr.map(fn)) // 并发 + 容错
for (const x of arr) await fn(x) // 串行
13.3 复杂度速查
| 操作 | 复杂度 |
|---|---|
push / pop |
O(1) |
shift / unshift |
O(n) |
下标访问 arr[i] |
O(1) |
indexOf / includes / find |
O(n) |
slice / concat / map / filter |
O(n) |
splice 中间插入/删除 |
O(n) |
sort |
O(n log n) |
十四、总结
把上面的内容压缩成 6 条核心原则:
-
先想清楚「要不要改原数组」
会改的只有 9 个:
push pop shift unshift splice sort reverse fill copyWithin。其余都返回新值。不确定就用 ES2023 的
toSorted/toReversed/toSpliced/with。 -
选方法看语义,不是看熟悉度
- 找一个 用
find,别用filter()[0] - 判断存在 用
includes/some,别用indexOf() > -1 - 映射 用
map,筛选 用filter,聚合 用reduce
- 找一个 用
-
reduce很强,但别滥用一行看不懂的
reduce不如写for循环。 -
异步永远记住:
map负责并发,Promise.all负责收口forEach+async是错误写法,不是「不推荐」。 -
注意那些不报错但结果不对的坑
sort()默认字符串排序、indexOf找不到NaN、空数组every返回true、new Array(n).map()无效------这些都不会抛错,只会静默出错。 -
性能问题最后再考虑
99% 的业务场景里,可读性 >> 那几毫秒。但大数据量 + O(n²) 的组合(比如循环里套
find)必须用Map优化。
如果这篇文章帮到你,欢迎点赞收藏。有问题评论区见。
本文代码均在现代浏览器(Chrome 110+)/ Node.js 18+ 环境下验证。
涉及 ES2023 / ES2024 特性的部分已标注兼容性,老项目请配合
core-js使用。