JavaScript 数组方法

深入解析 JavaScript 数组方法的机制差异:forEach/map/filter/reduce 的适用场景,find/some/every 的语义区别,以及 TypedArray 处理二进制数据的方法。

为什么数组方法是 JavaScript 开发的核心

数组是 JavaScript 最常用的数据结构之一。数组方法看似简单——mapfilterreduce 几乎每个前端工程师都在用——但背后的机制差异往往被忽视。

理解这些方法的设计意图和性能特征,才能写出既优雅又高效 JavaScript 代码。


一、forEach / map / filter / reduce

1.1 forEach — 纯粹的迭代

const arr = [1, 2, 3];

arr.forEach((value, index, array) => {
  console.log(`${index}: ${value}`);
});

// 注意:forEach 不支持 break/continue
// 如果需要提前退出,使用 for 循环或 every/some

forEach 的特点

  • 返回 undefined
  • 不修改原数组(除非回调中修改)
  • 不可链式调用
  • 不能 await(会并发执行所有回调)

1.2 map — 转换

const numbers = [1, 2, 3];

// map 返回新数组,长度与原数组相同
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6]

// 不应修改回调外的变量
const users = [{name: 'Alice'}, {name: 'Bob'}];
const names = users.map(user => user.name);
console.log(names); // ['Alice', 'Bob']

map 的特点

  • 总是返回新数组
  • 返回数组长度等于原数组长度
  • 元素一一对应
  • 适合做数据转换

1.3 filter — 筛选

const numbers = [1, 2, 3, 4, 5, 6];

// filter 返回满足条件的元素组成的新数组
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4, 6]

// 结合 map 使用
const squaredEvens = numbers
  .filter(x => x % 2 === 0)
  .map(x => x * x);
console.log(squaredEvens); // [4, 16, 36]

filter 的特点

  • 返回满足条件的元素
  • 不修改原数组
  • 返回数组长度可能小于原数组
  • 空数组返回空数组

1.4 reduce — 聚合

const numbers = [1, 2, 3, 4];

// reduce 接收一个reducer函数
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 10

// 计算对象数组的总价
const cart = [
  { item: 'Apple', price: 3, quantity: 2 },
  { item: 'Banana', price: 1, quantity: 5 }
];
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
console.log(total); // 11

reduce 的特点

  • 返回任何类型的值
  • 初始值可选(默认使用第一个元素)
  • 可以实现 map、filter、find 等所有其他方法
// 用 reduce 实现 map
const map = (arr, fn) => arr.reduce((acc, x) => [...acc, fn(x)], []);

// 用 reduce 实现 filter
const filter = (arr, fn) => arr.reduce((acc, x) => fn(x) ? [...acc, x] : acc, []);

1.5 性能对比

const largeArray = Array.from({ length: 100000 }, (_, i) => i);

// for 循环 — 最快
for (let i = 0; i < largeArray.length; i++) {
  largeArray[i] * 2;
}

// map — 会有函数调用开销,但代码更简洁
largeArray.map(x => x * 2);

// forEach — 类似于 map,但语义不同
largeArray.forEach(x => x * 2);

// 性能:for > forEach ≈ map > filter/reduce (取决于操作复杂度)

二、find / findIndex / some / every

2.1 find — 查找单个元素

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Alice' }
];

// find 返回第一个满足条件的元素
const user = users.find(u => u.name === 'Alice');
console.log(user); // { id: 1, name: 'Alice' }

// 找不到返回 undefined
const unknown = users.find(u => u.name === 'Charlie');
console.log(unknown); // undefined

2.2 findIndex — 查找位置

// findIndex 返回第一个满足条件的元素的索引
const index = users.findIndex(u => u.name === 'Alice');
console.log(index); // 0

// 找不到返回 -1
const notFound = users.findIndex(u => u.name === 'Charlie');
console.log(notFound); // -1

2.3 some — 是否有满足条件的元素

// some 返回布尔值,只要有一个满足就返回 true
const hasBob = users.some(u => u.name === 'Bob');
console.log(hasBob); // true

// 空数组永远返回 false
[].some(x => x > 0); // false

2.4 every — 是否所有元素都满足条件

// every 所有元素都满足才返回 true
const allAdults = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 17 }
];
const allAdult = allAdults.every(person => person.age >= 18);
console.log(allAdult); // false

// 空数组永远返回 true
[].every(x => x > 0); // true

2.5 语义区别总结

方法 返回值 找不到时 适用场景
find 元素本身 undefined 查找单个元素
findIndex 索引 -1 需要索引位置
some boolean false 检查是否存在
every boolean true 检查是否全部满足

三、flat / flatMap

3.1 flat — 展平嵌套数组

const nested = [1, [2, 3], [[4, 5]]];

// flat(depth) 展平指定深度
console.log(nested.flat());        // [1, 2, 3, [4, 5]]
console.log(nested.flat(2));       // [1, 2, 3, 4, 5]

// Infinity 展平到任意深度
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5]

// 自动移除空位
[1, 2, , 3].flat(); // [1, 2, 3]

3.2 flatMap — map + flat

const sentences = ['Hello world', 'How are you'];

// flatMap 等于 map + flat,但只遍历一次
const words = sentences.flatMap(s => s.split(' '));
console.log(words); // ['Hello', 'world', 'How', 'are', 'you']

// 经典场景:从数组中提取并展平
const requests = [
  { url: '/api/a', params: [1, 2] },
  { url: '/api/b', params: [3] }
];

// 错误做法:map + flatten
const wrong = requests.map(r => r.params.map(p => `${r.url}?id=${p}`)).flat();

// flatMap 更简洁
const correct = requests.flatMap(r =>
  r.params.map(p => `${r.url}?id=${p}`)
);

3.3 flatMap 的返回值特性

// flatMap 回调可以返回任何类型的元素
// 返回的元素会被自动展平(深度为 1)

// 返回数组:会被展平
[1, 2, 3].flatMap(x => [x, x * 2]); // [1, 2, 2, 4, 3, 6]

// 返回数字:不会被展平,结果是数字
[1, 2, 3].flatMap(x => x * 2); // [2, 4, 6]

// 返回 null/undefined:会被移除
[1, 2, 3].flatMap(x => x > 1 ? x : null); // [2, 3]

四、类数组对象与转换

4.1 什么是类数组对象

具有 length 属性和数字索引的对象,但不是真正的数组:

const arrayLike = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};

// 不能直接使用数组方法
// arrayLike.map(x => x) // TypeError

// 转换方法
const arr = Array.from(arrayLike);
console.log(arr); // ['a', 'b', 'c']

// 或者使用展开运算符(需要可迭代)
const arr2 = [...arrayLike]; // TypeError: arrayLike is not iterable

// 旧方法:Array.prototype.slice.call
const arr3 = Array.prototype.slice.call(arrayLike);

4.2 常见的类数组对象

// 函数 arguments
function fn() {
  console.log(Array.from(arguments)); // [1, 2, 3]
}
fn(1, 2, 3);

// DOM 元素集合
const divs = document.querySelectorAll('div');
Array.from(divs).forEach(div => { /* ... */ });

// 字符串
Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']

4.3 Array.from 的强大功能

// 基本转换
Array.from('abc'); // ['a', 'b', 'c']

// 带映射函数
Array.from('123', x => Number(x)); // [1, 2, 3]

// 创建数字序列
Array.from({ length: 5 }, (_, i) => i); // [0, 1, 2, 3, 4]

// 替代旧的不靠谱写法
Array(5).fill(0).map((_, i) => i); // [0, 1, 2, 3, 4]

五、TypedArray

5.1 为什么需要 TypedArray

JavaScript 数字是 64 位浮点数,存储效率低。TypedArray 提供高效存储和操作二进制数据的能力:

// 创建一个 5 个元素的 16 位有符号整数数组
const int16 = new Int16Array([1, 2, 3, 4, 5]);
console.log(int16.length); // 5
console.log(int16.BYTES_PER_ELEMENT); // 2

// 所有 TypedArray 类型
Int8Array;    // 8-bit 有符号整数
Uint8Array;    // 8-bit 无符号整数
Int16Array;    // 16-bit 有符号整数
Uint16Array;   // 16-bit 无符号整数
Int32Array;    // 32-bit 有符号整数
Uint32Array;   // 32-bit 无符号整数
Float32Array;  // 32-bit 浮点数
Float64Array;  // 64-bit 浮点数

5.2 使用场景:处理二进制数据

// 读取文件二进制数据
const buffer = new ArrayBuffer(8); // 8 字节
const view = new Uint8Array(buffer);

view[0] = 0x48; // 'H'
view[1] = 0x69; // 'i'

// DataView:灵活读取不同类型
const dataView = new DataView(buffer);
console.log(dataView.getUint8(0)); // 72
console.log(dataView.getUint16(0)); // 0x6948 = 26952 (大端序)

// 指定小端序
console.log(dataView.getUint16(0, true)); // 0x4869 = 18537 (小端序)

5.3 ArrayBuffer 与 DataView

// ArrayBuffer:原始二进制数据缓冲区
const buffer = new ArrayBuffer(16);

// DataView:在 ArrayBuffer 上灵活读写
const dataView = new DataView(buffer);
dataView.setInt32(0, 42, true);  // 小端序写入
dataView.setFloat64(4, 3.14, true); // 小端序写入

console.log(dataView.getInt32(0, true));   // 42
console.log(dataView.getFloat64(4, true)); // 3.14

六、延展阅读