第52课:数组与常用方法——遍历、map/filter/reduce、splice/slice、类数组
数组是 JavaScript 中最常用的数据结构,用于存储有序的、可索引的数据集合。JavaScript 的数组是动态的(长度可变)、无类型限制的(一个数组内可混合存放任何类型),并且拥有超过 30 个内置方法。本节课将系统分类讲解数组的核心方法——从增删改查的基本操作,到 map/filter/reduce 等函数式迭代方法,再到数组与类数组对象之间的转换技巧。
1. 数组的创建与基础属性
1.1 创建数组的四种方式
1 | // 字面量(最常用) |
new Array(n) 的陷阱:传入单个数字 n 时,表示创建一个长度为 n 的稀疏数组(元素全部为空位),而不是 [n]。为此 ES6 引入了 Array.of() 来解决这一歧义。
1.2 length 属性
数组的 length 属性不仅是只读的计数,还是可写的。修改 length 可截断数组或扩展数组(扩展部分为空位)。
1 | const arr = [1, 2, 3, 4, 5]; |
length 始终等于最大数字索引 + 1,而不是实际元素个数。
1 | const sparse = []; |
1.3 稀疏数组与空位
稀疏数组是指索引不连续的数组(中间有“空位”)。空位不同于 undefined 值——某些数组方法会跳过空位,而另一些会将其视为 undefined。
1 | const arr = [1, , 3]; // 索引 1 处是空位 |
实践建议:避免刻意创建稀疏数组。如果需要一个长度已知但初始值未知的数组,使用 Array(n).fill(0) 或用 Array.from({ length: n }, (_, i) => i) 填充明确的值。
2. 增删改查:修改原数组的方法(Mutator Methods)
以下方法会直接修改原数组,使用时应留意是否需要保留原始数据。
2.1 尾部操作:push、pop
1 | const arr = [1, 2]; |
2.2 头部操作:unshift、shift
1 | const arr = [2, 3]; |
性能提示:unshift 和 shift 需要移动数组中的所有元素,时间复杂度为 O(n)。相比之下,push 和 pop 仅操作末尾,时间复杂度为 O(1)。频繁在头部插入/删除时,考虑使用双向链表或改用索引从尾部操作。
2.3 splice:万能增删改
splice(start, deleteCount?, ...items?) 从指定位置删除若干元素,并可选择插入新元素。返回被删除元素组成的数组。
1 | const arr = [1, 2, 3, 4, 5]; |
2.4 其他修改方法
| 方法 | 行为 | 返回值 |
|---|---|---|
reverse() |
反转数组顺序。 | 修改后的原数组 |
sort(fn?) |
默认按字符串 Unicode 码点排序。传入比较函数自定义排序。 | 修改后的原数组 |
fill(v, s, e) |
用固定值填充 [s, e) 区间。 |
修改后的原数组 |
copyWithin(t, s, e) |
将 [s, e) 的元素复制到 t 位置。 |
修改后的原数组 |
1 | // sort 的数字排序陷阱 |
3. 不修改原数组的方法(Accessor & Iteration Methods)
3.1 slice:提取子数组
slice(start?, end?) 返回 [start, end) 区间的浅拷贝。支持负数索引(表示从末尾倒数)。
1 | const arr = [1, 2, 3, 4, 5]; |
将类数组转为数组的经典技巧(ES5 时代常用,现代使用 Array.from):
1 | function legacyConvert() { |
3.2 concat:合并数组
返回新数组,不修改原数组。
1 | const arr1 = [1, 2]; |
现代开发中,展开运算符 [...arr1, ...arr2] 更简洁,已大部分替代 concat。
3.3 indexOf / lastIndexOf / includes
| 方法 | 说明 | 返回值 |
|---|---|---|
indexOf(item, fromIndex?) |
查找第一次出现的索引,未找到返回 -1。 |
number |
lastIndexOf(item, fromIndex?) |
查找最后一次出现的索引。 | number |
includes(item, fromIndex?)(ES2016) |
判断是否包含元素。 | boolean |
这三个方法使用严格相等(===)比较。因此无法查找 NaN(NaN !== NaN),但 includes 经过特殊处理,可以正确检测 NaN。
1 | const arr = [1, NaN, 3]; |
3.4 find / findIndex / findLast / findLastIndex(ES2023)
1 | const users = [ |
4. 函数式迭代方法
这些方法接收一个回调函数作为参数,对数组的每个元素执行操作。它们不修改原数组。
4.1 forEach:仅遍历,无返回值
1 | const arr = [1, 2, 3]; |
注意:forEach 的回调中 return 无效(无法跳出循环),break 和 continue 也不可用。若需提前退出,应使用 for...of 或传统 for。
4.2 map:映射转换
对每个元素执行回调,将返回值组成新数组。新数组与原数组等长。
1 | const nums = [1, 2, 3]; |
4.3 filter:筛选过滤
返回回调返回 truthy 值的元素组成的新数组。
1 | const nums = [1, 2, 3, 4, 5, 6]; |
4.4 reduce:累积归并
reduce(callback(accumulator, currentValue, index, array), initialValue?) 将数组归并为一个值。这是一个极其强大且灵活的方法——map 和 filter 都可以用 reduce 实现。
1 | const nums = [1, 2, 3, 4]; |
始终提供初始值的理由:
- 如果数组为空且没有初始值,
reduce会抛出TypeError。 - 初始值明确了归并的预期类型(数字
0、数组[]、对象{})。
1 | // reduce 实现 map |
4.5 every / some
| 方法 | 行为 |
|---|---|
every |
所有元素都满足条件返回 true(全真才真)。 |
some |
任一元素满足条件返回 true(一真即真)。 |
两者都会短路:every 遇到第一个 falsy 回调就停止;some 遇到第一个 truthy 回调就停止。
1 | const nums = [2, 4, 6, 8]; |
4.6 flat / flatMap
flat(depth?) 按指定深度(默认 1)拍平嵌套数组。flatMap(callback) 等价于 map + 深度为 1 的 flat。
1 | const nested = [1, [2, [3, 4]]]; |
5. 类数组对象与 Array.from
类数组(Array-like)对象具有数字索引和 length 属性,但没有数组方法。常见的类数组有:
- 函数的
arguments对象(严格模式下已不推荐)。 - DOM 方法返回的
NodeList(如document.querySelectorAll)。 - 字符串(原始值,但可像类数组一样索引访问)。
1 | function demo() { |
Array.from 的第二个参数:可传入一个映射函数,对每个元素进行转换。
1 | // 生成 1 到 10 的数组 |
6. 数组解构与展开运算符
6.1 数组解构
1 | const [first, second, ...rest] = [10, 20, 30, 40, 50]; |
6.2 展开运算符在数组中的应用
1 | // 合并数组 |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 以下哪个方法会修改原数组?
A.slice
B.map
C.splice
D.filter(单选)
[3, 10, 1, 20].sort()的返回值是?
A.[1, 3, 10, 20]
B.[1, 10, 20, 3]
C.[1, 3, 10, 20](取决于浏览器)
D.[1, 10, 3, 20](填空) 要检测数组中是否包含
NaN,应使用______方法,因为indexOf无法检测NaN。(多选) 以下哪些方法在回调返回
false时会提前终止遍历?
A.forEach
B.every
C.some
D.map
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 函数 processOrders(orders),它接收一个订单对象数组,每个订单包含 { id, items: [{ name, price, quantity }], status }。要求如下:
- 使用
filter筛选出status === 'paid'的订单。 - 使用
map将每个订单转换为其所有商品总价(items.reduce累加price * quantity)。 - 使用
reduce计算所有已付款订单的总销售金额。 - 函数返回一个对象
{ paidCount, totalRevenue }。 - 使用
const声明函数,所有中间计算使用箭头函数。 - 添加 JSDoc 注释说明参数结构和返回值。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 filter、map、reduce 的组合使用方式。
三、面试真题与参考答案
题目(腾讯前端面试题):
请详细说明
for循环、forEach、map三者在遍历数组时的区别。包括能否中途退出、是否有返回值、对稀疏数组的处理,以及各自的适用场景。
参考答案:
| 特性 | for 循环 |
forEach |
map |
|---|---|---|---|
| 能否中途退出 | 可以(break / return) |
不能(return 仅相当于 continue) |
不能(同 forEach) |
| 返回值 | 无(手动收集结果) | undefined |
新数组,每个元素是回调的返回值 |
| 对稀疏数组的处理 | 访问空位返回 undefined |
跳过空位 | 保留空位(空位不调用回调,但输出数组保留空位) |
| 适用场景 | 需要精确控制索引、提前退出、或复杂迭代逻辑。 | 纯副作用操作(日志、DOM 更新)。 | 将数组的每个元素转换为新值,生成新数组。 |
| 语法风格 | 命令式 | 声明式 | 声明式(函数式) |
补充说明:
- 如果遍历中需要使用
await(异步操作),forEach会忽略回调返回的 Promise,导致异步执行顺序不可控。此时应使用for...of循环配合await。 map的设计目的是纯数据转换,不应在map回调中执行副作用(如修改外部变量、发送请求)。副作用应放在forEach或for...of中。
课后练习答案
一、概念自测答案
C
- 解析:
splice直接修改原数组并返回被删除的元素。slice、map、filter均返回新数组。
- 解析:
D
- 解析:
sort()默认按字符串 Unicode 码点排序。[3, 10, 1, 20]转字符串后排序结果为[1, 10, 20, 3]。
- 解析:
includes- 解析:
includes使用 SameValueZero 算法,能正确检测NaN。indexOf使用严格相等,无法检测。
- 解析:
B、C
- 解析:
every在回调返回false时短路,some在回调返回true时短路。forEach和map始终遍历每个元素。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数processOrders(orders),分析订单数据。要求:
- 参数
orders是数组,每个元素包含{ id, items: [{ name, price, quantity }], status }。- 使用
const声明函数,内部所有回调使用箭头函数。- 第一步:
orders.filter(order => order.status === 'paid')筛选已付款订单。- 第二步:对每个已付款订单,用
.map(order => order.items.reduce((sum, item) => sum + item.price * item.quantity, 0))计算该订单总价。- 第三步:
.reduce((total, orderTotal) => total + orderTotal, 0)计算总销售额。- 返回对象
{ paidCount: 已付款订单数, totalRevenue: 总销售额 }。- 添加 JSDoc 注释描述参数结构和返回值。
- 提供至少 3 个订单的测试数据及调用示例。直接输出完整代码。”