为什么数组方法是 JavaScript 开发的核心
数组是 JavaScript 最常用的数据结构之一。数组方法看似简单——map、filter、reduce 几乎每个前端工程师都在用——但背后的机制差异往往被忽视。
理解这些方法的设计意图和性能特征,才能写出既优雅又高效 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