WinddSnow

JavaScript-Arrays-and-Common-Methods

字数统计: 3.6k阅读时长: 15 min
2026/07/30

第52课:数组与常用方法——遍历、map/filter/reducesplice/slice、类数组

数组是 JavaScript 中最常用的数据结构,用于存储有序的、可索引的数据集合。JavaScript 的数组是动态的(长度可变)、无类型限制的(一个数组内可混合存放任何类型),并且拥有超过 30 个内置方法。本节课将系统分类讲解数组的核心方法——从增删改查的基本操作,到 map/filter/reduce 等函数式迭代方法,再到数组与类数组对象之间的转换技巧。


1. 数组的创建与基础属性

1.1 创建数组的四种方式

1
2
3
4
5
6
7
8
9
10
11
12
// 字面量(最常用)
const arr1 = [1, 2, 3];

// 构造函数
const arr2 = new Array(1, 2, 3); // [1, 2, 3]
const arr3 = new Array(5); // [empty × 5] —— 仅传入一个数字时,表示长度而非元素

// Array.of()(ES6,解决 new Array 的歧义)
const arr4 = Array.of(5); // [5]

// Array.from()(从类数组或可迭代对象创建)
const arr5 = Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']

new Array(n) 的陷阱:传入单个数字 n 时,表示创建一个长度为 n稀疏数组(元素全部为空位),而不是 [n]。为此 ES6 引入了 Array.of() 来解决这一歧义。

1.2 length 属性

数组的 length 属性不仅是只读的计数,还是可写的。修改 length 可截断数组或扩展数组(扩展部分为空位)。

1
2
3
4
5
6
const arr = [1, 2, 3, 4, 5];
arr.length = 3;
console.log(arr); // [1, 2, 3] —— 截断

arr.length = 5;
console.log(arr); // [1, 2, 3, empty × 2] —— 扩展为空位

length 始终等于最大数字索引 + 1,而不是实际元素个数。

1
2
3
const sparse = [];
sparse[100] = 'a';
console.log(sparse.length); // 101 —— 即使只有 1 个元素

1.3 稀疏数组与空位

稀疏数组是指索引不连续的数组(中间有“空位”)。空位不同于 undefined 值——某些数组方法会跳过空位,而另一些会将其视为 undefined

1
2
3
4
5
6
7
const arr = [1, , 3]; // 索引 1 处是空位
console.log(arr[1]); // undefined —— 访问空位返回 undefined

// 不同方法对空位的处理
arr.forEach(x => console.log(x)); // 1, 3 —— 跳过空位
console.log(arr.map(x => x * 2)); // [2, empty, 6] —— 保留空位
console.log(arr.filter(() => true)); // [1, 3] —— 跳过空位

实践建议:避免刻意创建稀疏数组。如果需要一个长度已知但初始值未知的数组,使用 Array(n).fill(0) 或用 Array.from({ length: n }, (_, i) => i) 填充明确的值。


2. 增删改查:修改原数组的方法(Mutator Methods)

以下方法会直接修改原数组,使用时应留意是否需要保留原始数据。

2.1 尾部操作:pushpop

1
2
3
const arr = [1, 2];
arr.push(3, 4); // 返回 4(新 length),arr 变为 [1, 2, 3, 4]
arr.pop(); // 返回 4(被移除的元素),arr 变为 [1, 2, 3]

2.2 头部操作:unshiftshift

1
2
3
const arr = [2, 3];
arr.unshift(0, 1); // 返回 4,arr 变为 [0, 1, 2, 3]
arr.shift(); // 返回 0,arr 变为 [1, 2, 3]

性能提示unshiftshift 需要移动数组中的所有元素,时间复杂度为 O(n)。相比之下,pushpop 仅操作末尾,时间复杂度为 O(1)。频繁在头部插入/删除时,考虑使用双向链表或改用索引从尾部操作。

2.3 splice:万能增删改

splice(start, deleteCount?, ...items?) 从指定位置删除若干元素,并可选择插入新元素。返回被删除元素组成的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = [1, 2, 3, 4, 5];

// 删除:从索引 2 开始删 2 个
const removed = arr.splice(2, 2);
console.log(removed); // [3, 4]
console.log(arr); // [1, 2, 5]

// 插入:从索引 1 开始删 0 个,插入新元素
arr.splice(1, 0, 'a', 'b');
console.log(arr); // [1, 'a', 'b', 2, 5]

// 替换:删 1 个,插 1 个
arr.splice(3, 1, 'X');
console.log(arr); // [1, 'a', 'b', 'X', 5]

2.4 其他修改方法

方法 行为 返回值
reverse() 反转数组顺序。 修改后的原数组
sort(fn?) 默认按字符串 Unicode 码点排序。传入比较函数自定义排序。 修改后的原数组
fill(v, s, e) 用固定值填充 [s, e) 区间。 修改后的原数组
copyWithin(t, s, e) [s, e) 的元素复制到 t 位置。 修改后的原数组
1
2
3
4
5
6
7
8
// sort 的数字排序陷阱
const nums = [3, 10, 1, 20];
nums.sort();
console.log(nums); // [1, 10, 20, 3] —— 字符串序!

// 正确的数字排序
nums.sort((a, b) => a - b);
console.log(nums); // [1, 3, 10, 20]

3. 不修改原数组的方法(Accessor & Iteration Methods)

3.1 slice:提取子数组

slice(start?, end?) 返回 [start, end) 区间的浅拷贝。支持负数索引(表示从末尾倒数)。

1
2
3
4
const arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 3)); // [2, 3]
console.log(arr.slice(-2)); // [4, 5] —— 最后两个
console.log(arr.slice()); // [1, 2, 3, 4, 5] —— 浅拷贝整个数组

将类数组转为数组的经典技巧(ES5 时代常用,现代使用 Array.from):

1
2
3
4
5
function legacyConvert() {
const args = Array.prototype.slice.call(arguments);
console.log(args); // [1, 2, 3]
}
legacyConvert(1, 2, 3);

3.2 concat:合并数组

返回新数组,不修改原数组。

1
2
3
4
const arr1 = [1, 2];
const arr2 = [3, 4];
const combined = arr1.concat(arr2, 5, [6, 7]);
console.log(combined); // [1, 2, 3, 4, 5, 6, 7]

现代开发中,展开运算符 [...arr1, ...arr2] 更简洁,已大部分替代 concat

3.3 indexOf / lastIndexOf / includes

方法 说明 返回值
indexOf(item, fromIndex?) 查找第一次出现的索引,未找到返回 -1 number
lastIndexOf(item, fromIndex?) 查找最后一次出现的索引。 number
includes(item, fromIndex?)(ES2016) 判断是否包含元素。 boolean

这三个方法使用严格相等===)比较。因此无法查找 NaNNaN !== NaN),但 includes 经过特殊处理,可以正确检测 NaN

1
2
3
const arr = [1, NaN, 3];
console.log(arr.indexOf(NaN)); // -1 —— NaN !== NaN
console.log(arr.includes(NaN)); // true —— includes 能检测 NaN

3.4 find / findIndex / findLast / findLastIndex(ES2023)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];

const user = users.find(u => u.name === 'Bob');
console.log(user); // { id: 2, name: 'Bob' }

const index = users.findIndex(u => u.name === 'Bob');
console.log(index); // 1

// 未找到时
console.log(users.find(u => u.name === 'X')); // undefined
console.log(users.findIndex(u => u.name === 'X')); // -1

4. 函数式迭代方法

这些方法接收一个回调函数作为参数,对数组的每个元素执行操作。它们不修改原数组

4.1 forEach:仅遍历,无返回值

1
2
3
4
5
6
7
const arr = [1, 2, 3];
arr.forEach((item, index, array) => {
console.log(`${index}: ${item}`);
});
// 0: 1
// 1: 2
// 2: 3

注意forEach 的回调中 return 无效(无法跳出循环),breakcontinue 也不可用。若需提前退出,应使用 for...of 或传统 for

4.2 map:映射转换

对每个元素执行回调,将返回值组成新数组。新数组与原数组等长。

1
2
3
4
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(nums); // [1, 2, 3] —— 原数组不变

4.3 filter:筛选过滤

返回回调返回 truthy 值的元素组成的新数组。

1
2
3
const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6]

4.4 reduce:累积归并

reduce(callback(accumulator, currentValue, index, array), initialValue?) 将数组归并为一个值。这是一个极其强大且灵活的方法——mapfilter 都可以用 reduce 实现。

1
2
3
4
5
6
7
8
9
const nums = [1, 2, 3, 4];

// 求和
const sum = nums.reduce((acc, n) => acc + n, 0);
console.log(sum); // 10

// 不使用初始值时,第一次迭代 accumulator 为第一个元素,currentValue 为第二个
const sum2 = nums.reduce((acc, n) => acc + n);
console.log(sum2); // 10(同样结果)

始终提供初始值的理由

  • 如果数组为空且没有初始值,reduce 会抛出 TypeError
  • 初始值明确了归并的预期类型(数字 0、数组 []、对象 {})。
1
2
3
4
5
6
7
8
9
10
11
12
13
// reduce 实现 map
const doubled = nums.reduce((acc, n) => { acc.push(n * 2); return acc; }, []);

// reduce 实现 filter
const evens = nums.reduce((acc, n) => { if (n % 2 === 0) acc.push(n); return acc; }, []);

// reduce 计数
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }

4.5 every / some

方法 行为
every 所有元素都满足条件返回 true(全真才真)。
some 任一元素满足条件返回 true(一真即真)。

两者都会短路every 遇到第一个 falsy 回调就停止;some 遇到第一个 truthy 回调就停止。

1
2
3
const nums = [2, 4, 6, 8];
console.log(nums.every(n => n % 2 === 0)); // true
console.log(nums.some(n => n > 5)); // true

4.6 flat / flatMap

flat(depth?) 按指定深度(默认 1)拍平嵌套数组。flatMap(callback) 等价于 map + 深度为 1 的 flat

1
2
3
4
5
6
7
const nested = [1, [2, [3, 4]]];
console.log(nested.flat()); // [1, 2, [3, 4]]
console.log(nested.flat(2)); // [1, 2, 3, 4]

const sentences = ['Hello world', 'I love JS'];
const words = sentences.flatMap(s => s.split(' '));
console.log(words); // ['Hello', 'world', 'I', 'love', 'JS']

5. 类数组对象与 Array.from

类数组(Array-like)对象具有数字索引和 length 属性,但没有数组方法。常见的类数组有:

  • 函数的 arguments 对象(严格模式下已不推荐)。
  • DOM 方法返回的 NodeList(如 document.querySelectorAll)。
  • 字符串(原始值,但可像类数组一样索引访问)。
1
2
3
4
5
6
7
8
9
function demo() {
console.log(arguments); // Arguments(3) [1, 2, 3] —— 有 length 和索引,但不是数组
// arguments.map(x => x * 2); // TypeError: arguments.map is not a function

// 转为真正的数组
const args = Array.from(arguments);
console.log(args.map(x => x * 2)); // [2, 4, 6]
}
demo(1, 2, 3);

Array.from 的第二个参数:可传入一个映射函数,对每个元素进行转换。

1
2
3
// 生成 1 到 10 的数组
const oneToTen = Array.from({ length: 10 }, (_, i) => i + 1);
console.log(oneToTen); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

6. 数组解构与展开运算符

6.1 数组解构

1
2
3
4
5
6
7
8
const [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first); // 10
console.log(second); // 20
console.log(rest); // [30, 40, 50]

// 跳过元素
const [, , third] = [1, 2, 3, 4];
console.log(third); // 3

6.2 展开运算符在数组中的应用

1
2
3
4
5
6
7
8
// 合并数组
const combined = [...[1, 2], ...[3, 4]]; // [1, 2, 3, 4]

// 函数参数展开
Math.max(...[10, 5, 30]); // 30

// 浅拷贝
const copy = [...original];

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 以下哪个方法会修改原数组?
    A. slice
    B. map
    C. splice
    D. filter

  2. (单选) [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]

  3. (填空) 要检测数组中是否包含 NaN,应使用 ______ 方法,因为 indexOf 无法检测 NaN

  4. (多选) 以下哪些方法在回调返回 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 代码。提示词中需明确指定 filtermapreduce 的组合使用方式。

三、面试真题与参考答案

题目(腾讯前端面试题):

请详细说明 for 循环、forEachmap 三者在遍历数组时的区别。包括能否中途退出、是否有返回值、对稀疏数组的处理,以及各自的适用场景。

参考答案

特性 for 循环 forEach map
能否中途退出 可以(break / return 不能return 仅相当于 continue 不能(同 forEach
返回值 无(手动收集结果) undefined 新数组,每个元素是回调的返回值
对稀疏数组的处理 访问空位返回 undefined 跳过空位 保留空位(空位不调用回调,但输出数组保留空位)
适用场景 需要精确控制索引、提前退出、或复杂迭代逻辑。 纯副作用操作(日志、DOM 更新)。 将数组的每个元素转换为新值,生成新数组。
语法风格 命令式 声明式 声明式(函数式)

补充说明

  • 如果遍历中需要使用 await(异步操作),forEach 会忽略回调返回的 Promise,导致异步执行顺序不可控。此时应使用 for...of 循环配合 await
  • map 的设计目的是纯数据转换,不应在 map 回调中执行副作用(如修改外部变量、发送请求)。副作用应放在 forEachfor...of 中。

课后练习答案

一、概念自测答案

  1. C

    • 解析:splice 直接修改原数组并返回被删除的元素。slicemapfilter 均返回新数组。
  2. D

    • 解析:sort() 默认按字符串 Unicode 码点排序。[3, 10, 1, 20] 转字符串后排序结果为 [1, 10, 20, 3]
  3. includes

    • 解析:includes 使用 SameValueZero 算法,能正确检测 NaNindexOf 使用严格相等,无法检测。
  4. B、C

    • 解析:every 在回调返回 false 时短路,some 在回调返回 true 时短路。forEachmap 始终遍历每个元素。

二、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 个订单的测试数据及调用示例。直接输出完整代码。”
CATALOG
  1. 1. 第52课:数组与常用方法——遍历、map/filter/reduce、splice/slice、类数组
    1. 1.1. 1. 数组的创建与基础属性
      1. 1.1.1. 1.1 创建数组的四种方式
      2. 1.1.2. 1.2 length 属性
      3. 1.1.3. 1.3 稀疏数组与空位
    2. 1.2. 2. 增删改查:修改原数组的方法(Mutator Methods)
      1. 1.2.1. 2.1 尾部操作:push、pop
      2. 1.2.2. 2.2 头部操作:unshift、shift
      3. 1.2.3. 2.3 splice:万能增删改
      4. 1.2.4. 2.4 其他修改方法
    3. 1.3. 3. 不修改原数组的方法(Accessor & Iteration Methods)
      1. 1.3.1. 3.1 slice:提取子数组
      2. 1.3.2. 3.2 concat:合并数组
      3. 1.3.3. 3.3 indexOf / lastIndexOf / includes
      4. 1.3.4. 3.4 find / findIndex / findLast / findLastIndex(ES2023)
    4. 1.4. 4. 函数式迭代方法
      1. 1.4.1. 4.1 forEach:仅遍历,无返回值
      2. 1.4.2. 4.2 map:映射转换
      3. 1.4.3. 4.3 filter:筛选过滤
      4. 1.4.4. 4.4 reduce:累积归并
      5. 1.4.5. 4.5 every / some
      6. 1.4.6. 4.6 flat / flatMap
    5. 1.5. 5. 类数组对象与 Array.from
    6. 1.6. 6. 数组解构与展开运算符
      1. 1.6.1. 6.1 数组解构
      2. 1.6.2. 6.2 展开运算符在数组中的应用
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)