《JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南》

JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南

数组是 JS 里使用频率最高的数据结构,但很多人只停留在 map / filter / forEach 三板斧。
这篇文章从方法分类讲到实战配方,再到异步并发性能避坑,一次性讲透。
适合:前端开发、面试复习、代码重构参考。


目录


一、先搞懂数组的本质

1.1 数组到底是什么

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,超出会变成普通属性
Array.isArray([])        // true
Array.isArray(new Set()) // false
Array.isArray({})        // false
Array.isArray('abc')     // false  ← 字符串不是数组

1.2 创建数组的 7 种方式

// 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跳过空洞。想填充必须用 fillArray.from

1.3 稀疏数组(holes)—— 90% 的人没注意的坑

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

返回值速记

方法找不到 / 空数组时返回
findundefined
findIndex / indexOf / lastIndexOf-1
filter / map / flat / slice[]
somefalse
everytrue(空真命题)
reduce(无初始值)空数组直接抛 TypeError

口诀:除了那 9 个「破坏性」方法,其余都返回新数组或新值,原数组安全。


三、增删改:9 个会修改原数组的方法

3.1 push / pop —— 尾部操作,O(1)

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 合并数组的正确姿势

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)

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() 会卡死页面。大数据量请改用双端队列倒序遍历

// 从头部逐个取出的高性能写法
for (let i = 0; i < arr.length; i++) {
  const item = arr[i]   // 用下标访问代替 shift
}

3.3 splice —— 瑞士军刀(删除 / 插入 / 替换)

// 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 三大经典用法

// ① 删除指定下标
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 都会漏掉它。

const a = [1, 2, 3]
delete a[1]
console.log(a)       // [1, empty, 3]
console.log(a.length) // 3  ← 长度没变!

3.4 sort —— 排序(坑最多)

const nums = [10, 9, 100]

nums.sort()                       // ❌ [10, 100, 9]

为什么? sort() 不传比较函数时,会把每个元素先 String() 转换,再按 UTF-16 码点排序。'10' < '100' < '9'

nums.sort((a, b) => a - b)        // ✅ [9, 10, 100]  升序
nums.sort((a, b) => b - a)        // ✅ [100, 10, 9]  降序

6 种实战排序场景

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+ 原生保证)
// 排序前后同值元素的相对顺序不变

洗牌的正确与错误写法

// ❌ 错误:分布不均匀,某些排列永远不会出现
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
}

不改原数组的排序

const sorted1 = [...arr].sort((a, b) => a - b)   // ES6
const sorted2 = arr.toSorted((a, b) => a - b)    // ES2023 ✅ 推荐

3.5 reverse —— 反转

const a = [1, 2, 3]
a.reverse()          // 返回原数组(已反转),a → [3, 2, 1]

// 不改原数组
const rev1 = [...a].reverse()
const rev2 = a.toReversed()   // ES2023

3.6 fill —— 填充

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.frommap 生成新数组。

3.7 copyWithin —— 内部复制

// 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 的妙用(清空 / 截断)

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)

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 的坑

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 === NaNfalseincludesSameValueZero 算法,能匹配 NaN

场景indexOfincludes
NaN-1true
undefined(洞)-1true
需要下标❌ 不支持
语义清晰度一般更好

结论:只判断「有没有」用 includes;需要下标用 indexOf / findIndex

// 判断数组是否包含对象的某个值
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

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 性能对比

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 —— 真假判断

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 来源。做校验前先判空:

const isValid = list.length > 0 && list.every(check)

短路特性(性能优化点):

// 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
})

实际用途

// 表单校验
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 查找的三个衍生问题

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 四种遍历方式对比

特性forfor...offorEachmap
能否 break / continue
能否 await❌ 语义错误❌ 语义错误
能否 return 跳出函数❌ 只能跳回调
是否返回新数组❌(undefined
能否跳过空洞❌ 会遍历❌ 会遍历✅ 跳过✅ 跳过
性能最快较快中等中等
可读性一般

5.2 forEach 的三个致命限制

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 选择建议

// 只是执行副作用(打印、写 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 的区别

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...offor...in 留给对象。


六、转换:map / filter / slice / flat / flatMap

6.1 map —— 1:1 映射

// 基本用法:长度恒定不变
[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 个高阶用法

// ① 提取 + 重命名
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 —— 条件筛选

[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 —— 截取(不改原数组)

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)

⚠️ slicesplice 千万别搞混

  • slice:截取,不改原数组,返回截取的部分
  • splice原数组,返回被删掉的部分
const 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 —— 拍平

// 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]  顺便实现"条件过滤 + 映射"

flatMapmap().flat() 好在哪:少创建一次中间数组,且只需遍历一遍。

6.5 concat / 展开运算符 —— 合并

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 —— 转字符串

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 —— 迭代器

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 参数与执行流程

arr.reduce((accumulator, currentValue, currentIndex, array) => {
  // 返回值会作为下一轮的 accumulator
  return newAccumulator
}, initialValue)

逐轮执行图解

[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

不传初始值时

[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 初始值陷阱

[].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 个实战场景

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 的两点提醒

  1. 可读性reduce 很强大,但过度使用会变成「密码」。如果一个 reduce 需要写注释才能看懂,for 循环可能是更好的选择。
  2. 性能reduce 因为有闭包调用和每轮的返回值传递,通常比 for1.5 ~ 3 倍。1000 万次求和实测:for 约 15ms,reduce 约 45ms。但业务代码里这点差距通常不重要。

八、ES2022 ~ ES2024 新 API

8.1 Array.prototype.at(ES2022)

const arr = [10, 20, 30]
arr.at(-1)      // 30
arr.at(-2)      // 20

8.2 findLast / findLastIndex(ES2023)

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 / 赋值。

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] = varr.with(i, v)

为什么重要:在 Vue / React 这类依赖「引用变化」触发更新的框架里,不可变方法能让数据流更可预测:

// 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-js polyfill 或使用 browserslist 配置转译。

8.4 Object.groupBy / Map.groupBy(ES2024)

以前分组只能写 reduce,现在有官方 API:

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

const grouped = Map.groupBy(users, u => u.dept)
grouped.get('研发')   // [张三, 李四]

8.5 Array.fromAsync(ES2025)

把异步可迭代对象转成数组:

const arr = await Array.fromAsync(asyncGenerator())
const arr2 = await Array.fromAsync(
  { length: 3 },
  async (_, i) => await fetchItem(i)
)

兼容性较新(Chrome 121+),生产环境慎用。


九、30 个高频实战场景

9.1 去重与集合运算

// ① 基础类型去重
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 统计计算

// ⑧ 求和
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 分组与转换

// ⑬ 分组(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 切分与重组

// ⑳ 分块(每 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 删除与更新

// ㉖ 按值删除
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 其他实用工具

// 判断两个数组是否相等(浅比较)
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

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(并发)

// ✅ 并发执行,全部完成后拿到结果,且顺序与 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.all200ms(并发)
for...of + await2000ms(串行)

10.3 允许部分失败:Promise.allSettled

Promise.all 有个致命特性:只要有一个 Promise reject,整体立刻 reject,其它已完成的结果全部丢弃

// 常见做法一:让子任务自己 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)
// 常见做法二:用 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 串行执行(后一个依赖前一个)

// 方式一: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 更宽但服务端仍有限制)
  • 打爆后端,触发限流
// 手写限流器(不依赖第三方库)
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))

或者用成熟的库

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 —— 消费异步迭代器

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 一个真实案例

需求:从设备树里挑出「地图上已配置坐标」的分组,并发拉取每组的摄像头列表,失败的跳过。

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
  }
}

这段代码的三个要点

  1. filter白名单预筛,避免无意义的网络请求
  2. map 把「分组数组」变成「Promise 数组」,实现并发
  3. 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...of120 ms
reduce45 ms
forEach40 ms

结论:map / filter / reduce 不比 for。它们牺牲了少量性能,换来可读性、不可变性和顺序保证

11.2 什么时候该用 for

// ① 超大数组的密集计算(10 万条以上)
// ② 循环体内逻辑复杂、需要多处 break / continue
// ③ 需要提前终止且不想引入异常
// ④ 追求极致内存(避免中间数组)

11.3 减少中间数组

// ❌ 产生 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

// ❌ 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 个避坑指南

#说明正确做法
1sort() 默认字符串排序[10,9,100].sort()[10,100,9]永远传比较函数 (a,b) => a-b
2reduce 忘传初始值空数组直接 TypeError永远显式传初始值
3indexOf 找不到 NaN=== 比较includes
4new Array(n).map() 无效map 会跳过空洞Array.from({length:n}, fn)
5forEach + async收不到结果、异常丢失map + Promise.all
6map(async ...)Promise.all拿到的是 Promise 数组必须包 Promise.all
7slice / splice 搞混splice 改原数组且返回被删元素要新数组用 slice
8sort / reverse 破坏原数组影响其它引用该数组的逻辑[...arr].sort() / toSorted()
9浅拷贝后改嵌套对象嵌套对象仍共享引用structuredClone 深拷贝
10push 返回长度不是数组无法链式调用分开写
11delete arr[i]留下空洞,length 不变splice
12[] .every() 返回 true空真命题,校验会失效list.length > 0
13includes 比对象失效引用类型比的是地址some / findIndex
14map(u => { name: u.name }){} 被当函数体map(u => ({ name: u.name }))
15new Array(3).fill([])三行共享同一数组Array.from({length:3}, () => [])
16arr.length 在遍历中被改遍历范围已锁定,行为诡异遍历前先确定长度
17稀疏数组遍历次数不对forEach / map 跳过空洞Array.from 规整
18Vue 中直接改 props 数组违反单向数据流props.list.slice().sort()
19sort 是不稳定排序ES2019 前不保证同值顺序现代环境已稳定,老环境需加 tie-breaker
20== 判断数组相等引用比较,永远 falseJSON.stringify 或逐项比较

关于第 18 条(Vue 场景)展开

// ❌ 直接改 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)
// 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 / awaitfor...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 一页纸记忆卡

// ===== 会改原数组的 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 / popO(1)
shift / unshiftO(n)
下标访问 arr[i]O(1)
indexOf / includes / findO(n)
slice / concat / map / filterO(n)
splice 中间插入/删除O(n)
sortO(n log n)

十四、总结

把上面的内容压缩成 6 条核心原则

  1. 先想清楚「要不要改原数组」
    会改的只有 9 个:push pop shift unshift splice sort reverse fill copyWithin。其余都返回新值。
    不确定就用 ES2023 的 toSorted / toReversed / toSpliced / with

  2. 选方法看语义,不是看熟悉度

    • 一个find,别用 filter()[0]
    • 判断存在includes / some,别用 indexOf() > -1
    • 映射map筛选filter聚合reduce
  3. reduce 很强,但别滥用
    一行看不懂的 reduce 不如写 for 循环。

  4. 异步永远记住:map 负责并发,Promise.all 负责收口
    forEach + async错误写法,不是「不推荐」。

  5. 注意那些不报错但结果不对的坑
    sort() 默认字符串排序、indexOf 找不到 NaN、空数组 every 返回 truenew Array(n).map() 无效——这些都不会抛错,只会静默出错。

  6. 性能问题最后再考虑
    99% 的业务场景里,可读性 >> 那几毫秒。但大数据量 + O(n²) 的组合(比如循环里套 find)必须用 Map 优化。


如果这篇文章帮到你,欢迎点赞收藏。有问题评论区见。

本文代码均在现代浏览器(Chrome 110+)/ Node.js 18+ 环境下验证。
涉及 ES2023 / ES2024 特性的部分已标注兼容性,老项目请配合 core-js 使用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尾善爱看海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值