JavaScript 数组方法完全指南

从基础到进阶,覆盖所有常用场景

数组是 JavaScript 中最常用的数据结构之一。从 ES5 到 ES6+,数组方法经历了多次扩充,如今已经非常强大。熟练掌握数组方法,能让你的代码更简洁、更易读、更少出 Bug。本文系统梳理了 20+ 个数组方法,按用途分类讲解,并给出实战示例。

1. 遍历与迭代:forEach、map、filter、reduce

这四个是日常开发中使用频率最高的数组方法,也是函数式编程的基础。

map:转换数组

对数组中每个元素执行转换,返回一个新数组,长度和原数组相同。

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

// 每个元素乘 2
const doubled = nums.map(n => n * 2);
// [2, 4, 6, 8, 10]

// 提取对象属性
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const names = users.map(u => u.name);
// ['Alice', 'Bob']

// 异步 map(注意:map 不会等待 Promise)
const results = await Promise.all(
    ids.map(async id => await fetchUser(id))
);

filter:筛选数组

返回满足条件的元素组成的新数组。不改变原数组。

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

// 取偶数
const evens = nums.filter(n => n % 2 === 0);
// [2, 4, 6, 8]

// 过滤空值
const data = [1, null, 2, undefined, 3, ''];
const clean = data.filter(Boolean);
// [1, 2, 3]

// 数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = arr.filter((v, i, a) => a.indexOf(v) === i);
// [1, 2, 3]

reduce:万能转换

reduce 是最强大也最难掌握的数组方法。它可以把数组归约为任意类型的值:数字、对象、甚至另一个数组。

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

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

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

// 数组扁平化(单层)
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => [...acc, ...arr], []);
// [1, 2, 3, 4, 5]

链式调用

map、filter 都返回数组,因此可以链式调用,写出非常优雅的数据处理流水线:

const result = products
    .filter(p => p.price > 100)
    .map(p => ({ ...p, finalPrice: p.price * 0.8 }))
    .sort((a, b) => b.finalPrice - a.finalPrice)
    .slice(0, 10);

2. 查找与判断:find、some、every、includes

const users = [
    { id: 1, name: 'Alice', age: 25 },
    { id: 2, name: 'Bob', age: 30 },
    { id: 3, name: 'Charlie', age: 35 }
];

// find:找到第一个匹配的元素
const alice = users.find(u => u.name === 'Alice');
// { id: 1, name: 'Alice', age: 25 }

// findIndex:找到第一个匹配元素的索引
const idx = users.findIndex(u => u.age > 30);
// 2

// some:是否至少有一个满足条件
const hasSenior = users.some(u => u.age >= 60);
// false

// every:是否所有元素都满足条件
const allAdult = users.every(u => u.age >= 18);
// true

// includes:是否包含某个值(基本类型)
[1, 2, 3].includes(2);  // true
[1, 2, 3].includes(4);  // false

小技巧:用 some 配合 ! 替代 every 有时更高效——some 在找到第一个匹配就停止,而 every 需要遍历所有元素。

3. 增删改:push、pop、splice、slice

注意区分:改变原数组的方法(push、pop、shift、unshift、splice、sort、reverse)和不改变原数组的方法(slice、concat、map、filter)。在 React 等不可变数据范式中,优先使用不改变原数组的方法。

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

// push/pop 操作尾部(O(1))
arr.push(6);   // [1,2,3,4,5,6]
arr.pop();     // [1,2,3,4,5]

// shift/unshift 操作头部(O(n),性能较差)
arr.unshift(0); // [0,1,2,3,4,5]
arr.shift();    // [1,2,3,4,5]

// slice:切片,不改变原数组
arr.slice(1, 3);   // [2, 3]
arr.slice(-2);     // [4, 5]

// splice:删除/插入/替换,改变原数组
arr.splice(2, 1);          // 删除索引2的1个元素 → [1,2,4,5]
arr.splice(2, 0, 'a');     // 在索引2插入 'a' → [1,2,'a',4,5]
arr.splice(1, 2, 'x','y'); // 替换 → [1,'x','y',4,5]

4. 排序:sort 的正确用法

JavaScript 的 sort 默认按字符串 Unicode 码点排序,这是最容易踩坑的地方:

// ❌ 错误:数字被当成字符串比较
[10, 2, 20, 1].sort();
// [1, 10, 2, 20]

// ✅ 正确:传入比较函数
[10, 2, 20, 1].sort((a, b) => a - b);  // 升序
// [1, 2, 10, 20]
[10, 2, 20, 1].sort((a, b) => b - a);  // 降序
// [20, 10, 2, 1]

// 对象数组排序
const users = [{name: 'Bob', age: 30}, {name: 'Alice', age: 25}];
users.sort((a, b) => a.age - b.age);  // 按年龄升序

// 字符串排序(中文用 localeCompare)
users.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));

注意:sort 会改变原数组!如果不想改变原数组,先复制一份:[...arr].sort(...)

5. 实用技巧与常见坑

数组去重的几种方式

// 1. Set 最简单(基本类型)
const unique = [...new Set(arr)];

// 2. filter + indexOf(保留首次出现位置)
const unique = arr.filter((v, i, a) => a.indexOf(v) === i);

// 3. 对象数组按某个字段去重
const seen = new Set();
const unique = users.filter(u => {
    if (seen.has(u.id)) return false;
    seen.add(u.id);
    return true;
});

常见坑点

更多数组调试技巧,可以使用 DevToolHub 的 JSON 格式化工具查看数组结构,方便地进行格式化和校验。

6. ES2019+ 新增方法

JavaScript 标准一直在演进,近几年新增了不少实用的数组方法,了解它们可以让代码更简洁。

// flat():数组扁平化
const arr = [1, [2, [3, [4]]]];
arr.flat();      // [1, 2, [3, [4]]]  扁平化一层
arr.flat(2);     // [1, 2, 3, [4]]    扁平化两层
arr.flat(Infinity); // [1, 2, 3, 4]   全部扁平化

// flatMap():map + flat 的组合,性能更好
const words = ['hello world', 'good morning'];
words.flatMap(w => w.split(' '));
// ['hello', 'world', 'good', 'morning']

// at():支持负索引(终于!)
const arr = [1, 2, 3, 4, 5];
arr.at(-1);  // 5  最后一个元素
arr.at(-2);  // 4  倒数第二个
arr.at(0);   // 1  第一个

// toReversed() / toSorted() / toSpliced() / with()
// ES2023 新增的不可变方法,返回新数组
const arr = [3, 1, 2];
arr.toReversed();  // [2, 1, 3]  arr 不变
arr.toSorted();    // [1, 2, 3]  arr 不变
arr.toSpliced(1, 1); // [3, 2]   arr 不变
arr.with(0, 100);  // [100, 1, 2] arr 不变

// findLast() / findLastIndex():从后往前找
const users = [
  { name: 'Alice', active: false },
  { name: 'Bob', active: true },
  { name: 'Charlie', active: true }
];
users.findLast(u => u.active);  // { name: 'Charlie', active: true }
users.findLastIndex(u => u.active);  // 2

这些新方法中,at() 和不可变数组方法特别实用。at(-1) 终于解决了 JavaScript 数组取最后一个元素不够优雅的问题(不再需要 arr[arr.length - 1])。而 toReversed/toSorted/toSpliced 等不可变方法,让 React 等需要不可变更新的场景写起来更方便。

7. 数组与迭代器

ES6 引入的迭代器和生成器让数组的遍历方式更加灵活。理解迭代器可以帮助你写出更通用的代码。

// 三个返回迭代器的方法
const arr = ['a', 'b', 'c'];

// keys() - 键的迭代器
[...arr.keys()];  // [0, 1, 2]

// values() - 值的迭代器
[...arr.values()];  // ['a', 'b', 'c']

// entries() - 键值对的迭代器
[...arr.entries()];  // [[0,'a'], [1,'b'], [2,'c']]

// for...of + entries 同时获取索引和值
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}

// 类数组转数组
Array.from(arrayLike);           // 通用方法
Array.from('hello');             // ['h','e','l','l','o']
Array.from({length: 5}, (_, i) => i * 2); // [0, 2, 4, 6, 8]

// Array.of - 创建数组(解决 Array(3) 的歧义)
Array.of(3);     // [3]
Array.of(1,2,3); // [1, 2, 3]

8. 性能考量

数组方法虽然方便,但在处理超大规模数据时需要考虑性能。以下是一些性能优化建议:

不过,过早优化是万恶之源。大多数场景下,代码的可读性比那点性能差异重要得多。只有在确实遇到性能瓶颈时,再考虑用性能更好但可读性稍差的写法。

9. 类型化数组(TypedArray)

处理大量二进制数据时,普通数组的性能和内存占用都不理想。ES6 引入的类型化数组可以操作原始二进制数据,性能更好。

// 创建 8 位无符号整数数组(每个元素 1 字节)
const uint8 = new Uint8Array(1024);
uint8[0] = 255;

// 其他类型
new Int8Array(10);     // 8位有符号整数
new Uint16Array(10);   // 16位无符号整数
new Int32Array(10);    // 32位有符号整数
new Float32Array(10);  // 32位浮点数
new Float64Array(10);  // 64位浮点数(和普通 Number 一样)

// 从 ArrayBuffer 创建
const buffer = new ArrayBuffer(16);  // 16 字节
const view = new DataView(buffer);
view.setUint8(0, 255);
view.setUint32(1, 0x12345678);
view.getFloat64(8);

类型化数组在处理文件、Canvas 像素数据、WebSocket 二进制消息、WebGL 数据等场景中非常重要。它们不是普通数组——没有 push/pop 方法,长度固定,但访问速度更快,内存占用更小。

10. 常见面试题

数组相关的面试题层出不穷,以下是几道经典题目和解题思路:

// 1. 数组去重
const unique = [...new Set(arr)];  // 最简单,基本类型

// 2. 数组扁平化
const flatten = arr => arr.flat(Infinity);  // ES2019
// 递归版
const flatten = arr => arr.reduce(
    (acc, v) => acc.concat(Array.isArray(v) ? flatten(v) : v),
    []
);

// 3. 数组交集
const intersection = (a, b) => {
    const set = new Set(b);
    return a.filter(x => set.has(x));
};

// 4. 数组并集
const union = (a, b) => [...new Set([...a, ...b])];

// 5. 数组差集(A 有但 B 没有)
const difference = (a, b) => {
    const set = new Set(b);
    return a.filter(x => !set.has(x));
};

// 6. 找出数组中出现次数最多的元素
function mostFrequent(arr) {
    const map = new Map();
    let max = 0, result = null;
    for (const item of arr) {
        const count = (map.get(item) || 0) + 1;
        map.set(item, count);
        if (count > max) {
            max = count;
            result = item;
        }
    }
    return result;
}

掌握这些基础操作,再面对更复杂的数组问题时就能游刃有余。面试中考察数组,核心是考察你对数据结构和算法复杂度的理解,而不是死记硬背 API。

11. 数组与对象的转换

实际开发中,数组和对象经常需要互相转换。掌握这些模式可以少写很多循环。

// 对象 → 数组
const obj = { a: 1, b: 2, c: 3 };

Object.keys(obj);     // ['a', 'b', 'c']
Object.values(obj);   // [1, 2, 3]
Object.entries(obj);  // [['a',1], ['b',2], ['c',3]]

// 数组 → 对象(用 Object.fromEntries)
const entries = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(entries);  // { a: 1, b: 2, c: 3 }

// 数组对象按 id 建索引(便于查找)
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}];
const userMap = new Map(users.map(u => [u.id, u]));
userMap.get(2);  // {id: 2, name: 'Bob'}  O(1) 查找

// 对象数组按字段分组
const items = [
  { category: 'fruit', name: 'apple' },
  { category: 'vegetable', name: 'carrot' },
  { category: 'fruit', name: 'banana' }
];

const grouped = items.reduce((acc, item) => {
  const key = item.category;
  if (!acc[key]) acc[key] = [];
  acc[key].push(item);
  return acc;
}, {});
// {
//   fruit: [{name:'apple'}, {name:'banana'}],
//   vegetable: [{name:'carrot'}]
// }

特别是按某个字段建立 Map 索引,在需要频繁查找时非常有用。把 O(n) 的查找变成 O(1),是很常用的性能优化技巧。

12. 数组的深浅拷贝

数组是引用类型,赋值只是复制了引用,修改一个会影响另一个。实际开发中经常需要复制数组。

// 浅拷贝(只复制一层)
const arr = [1, 2, 3];
const copy1 = [...arr];          // 展开运算符
const copy2 = arr.slice();       // slice 不传参
const copy3 = Array.from(arr);   // Array.from
const copy4 = arr.concat();      // concat 空数组

// 深拷贝(嵌套数组/对象也会被复制)
const nested = [[1, 2], [3, 4], { a: 1 }];

// 1. JSON 方法(简单场景,但不能处理函数、undefined、循环引用)
const deep1 = JSON.parse(JSON.stringify(nested));

// 2. structuredClone(现代浏览器原生支持,推荐)
const deep2 = structuredClone(nested);

// 3. 递归实现
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(deepClone);
  const result = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }
  return result;
}

structuredClone 是 ES2022 新增的 API,原生支持深拷贝,能处理 Date、RegExp、Map、Set、ArrayBuffer 等类型,比 JSON 方法功能强得多。如果不需要兼容 IE,强烈推荐使用。

13. 用迭代器处理异步流

异步迭代器(Async Iterator)是处理异步数据流的强大工具,特别适合分页加载、流式数据等场景。

// 异步生成器:逐页获取数据
async function* fetchAllPages() {
  let page = 1;
  while (true) {
    const data = await fetch(`/api/items?page=${page}`).then(r => r.json());
    if (data.items.length === 0) break;
    yield* data.items;  // 逐个产出数据项
    page++;
  }
}

// 使用 for await...of 遍历
async function processAll() {
  for await (const item of fetchAllPages()) {
    console.log('处理:', item);
  }
}

异步迭代器让异步数据流的处理变得和同步数组一样优雅。配合 fetch 的 ReadableStream、数据库游标等流式 API,可以写出非常简洁的异步数据处理代码。这也是 JavaScript 处理大数据量时的一个重要模式。

14. 数组方法的性能对比

你可能好奇,这么多数组方法哪个更快?实际测试下来,for 循环 > forEach > map/filter > reduce。但这个差异在数据量小于一万条时几乎可以忽略,真正有明显差异是在十万、百万级别的数据上。

所以日常开发中,优先选择语义最清晰、可读性最好的写法。等真正遇到性能问题时,再去做针对性优化。毕竟,代码是写给人看的,顺便给机器执行。