JavaScript 数组完全入门指南:从零基础到熟练掌握

适合初学者的数组学习指南,包含详细的概念解释和丰富的代码示例


前言

数组是编程中最基础也是最重要的数据结构之一。想象一下,你需要存储一个班级里所有学生的名字,或者记录一周七天的温度数据——这时候数组就派上用场了。简单来说,数组就是一个可以存放多个数据的容器,这些数据按照顺序排列,每个数据都有自己的"门牌号"(索引)。

在这篇文章中,我会从最基础的概念开始,一步步带你掌握 JavaScript 数组的各种用法。无论你是刚开始学习编程,还是想要系统地复习数组知识,这篇文章都能帮到你。


第一部分:什么是数组?

1.1 生活中的数组

在理解编程中的数组之前,让我们先看看生活中的例子:

  • 书架上的书:每本书都有一个位置(第1本、第2本、第3本…)
  • 日历上的日期:1号、2号、3号…按顺序排列
  • 储物柜:每个柜子都有编号,你可以根据编号找到对应的柜子

数组的原理和这些例子很像:它是一组按顺序排列的数据集合,每个数据都有一个对应的索引(位置编号)

1.2 数组的特点

JavaScript 的数组有以下几个重要特点:

  1. 有序性:数组中的元素按照添加的顺序排列
  2. 索引访问:通过索引(从 0 开始)可以快速访问任意元素
  3. 类型灵活:一个数组可以同时存放数字、字符串、对象等不同类型的数据
  4. 长度可变:可以随时添加或删除元素,数组长度会自动调整

1.3 数组的基本结构

// 想象这是一个书架,每个位置放着不同的书
const bookshelf = ['红楼梦', '西游记', '三国演义', '水浒传'];

// 索引:    0        1        2        3
// 对应:  第1本    第2本    第3本    第4本

// 通过索引获取元素
console.log(bookshelf[0]);  // 输出:'红楼梦'
console.log(bookshelf[2]);  // 输出:'三国演义'

重要提示:数组的索引从 0 开始,而不是 1。这意味着第一个元素的索引是 0,第二个是 1,以此类推。这是编程中的通用约定,刚开始可能会不习惯,但很快就会适应。


第二部分:创建数组的五种方式

在 JavaScript 中,创建数组有多种方法。作为初学者,你需要掌握最常用的几种。

2.1 数组字面量(最常用)

这是最简单、最直观的创建方式,使用方括号 [] 包裹元素,元素之间用逗号分隔。

// 创建一个空数组
const emptyArray = [];

// 创建包含数字的数组
const numbers = [1, 2, 3, 4, 5];

// 创建包含字符串的数组
const fruits = ['苹果', '香蕉', '橙子', '葡萄'];

// 创建混合类型的数组(JavaScript 允许这样做)
const mixed = [1, 'hello', true, null, { name: '张三' }];

// 数组可以嵌套——数组里放数组
const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];
// 这就像一个 3x3 的表格,matrix[0][0] 是 1,matrix[1][2] 是 6

为什么推荐这种方式?

  • 语法简洁明了
  • 阅读性好,一眼就能看出是个数组
  • 性能和其他方式没有差别

2.2 Array 构造函数

这是比较传统的创建方式,使用 new Array() 语法。

// 方式一:传入多个参数,每个参数成为数组的一个元素
const arr1 = new Array(1, 2, 3);
console.log(arr1);  // 输出:[1, 2, 3]

// 方式二:只传入一个数字参数,表示创建一个指定长度的空数组
const arr2 = new Array(5);
console.log(arr2);  // 输出:[empty × 5] —— 一个长度为5的空数组

// ⚠️ 注意这个容易混淆的地方:
const arr3 = new Array(3);      // 创建长度为3的空数组
const arr4 = new Array('3');    // 创建包含字符串'3'的数组 ['3']

构造函数的特殊行为解释

当你只给 Array 构造函数传一个数字时,JavaScript 会认为你想创建一个具有该长度的空数组。但如果传的是其他类型(比如字符串),它就会把那个值当作数组的元素。

由于这个容易混淆的特性,现代开发中更推荐使用数组字面量

2.3 Array.of() 方法(ES6)

这是 ES6(ECMAScript 2015)引入的新方法,它解决了构造函数的那个"坑"。

// Array.of 总是把参数当作数组元素,不管参数是什么类型
const arr1 = Array.of(3);       // [3] —— 包含数字3的数组
const arr2 = Array.of(1, 2, 3); // [1, 2, 3]
const arr3 = Array.of('hello'); // ['hello']

// 对比构造函数的区别
new Array(3);    // 创建长度为3的空数组
Array.of(3);     // 创建包含数字3的数组

什么时候用 Array.of?

  • 当你需要明确创建一个包含特定值的数组时
  • 在函数式编程中,配合其他高阶函数使用

2.4 Array.from() 方法(ES6)

这是一个非常强大的方法,可以把类数组对象可迭代对象转换成真正的数组。

// 1. 把字符串转换成字符数组
const chars = Array.from('hello');
console.log(chars);  // 输出:['h', 'e', 'l', 'l', 'o']

// 2. 把 Set 转换成数组(Set 是 ES6 的一种数据结构,自动去重)
const uniqueNums = Array.from(new Set([1, 2, 2, 3, 3, 3]));
console.log(uniqueNums);  // 输出:[1, 2, 3]

// 3. 使用映射函数——第二个参数可以对每个元素进行处理
const doubled = Array.from([1, 2, 3], num => num * 2);
console.log(doubled);  // 输出:[2, 4, 6]

// 4. 创建指定长度的数组并填充值
const tenOnes = Array.from({ length: 10 }, () => 1);
console.log(tenOnes);  // 输出:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

// 5. 创建 0 到 9 的数组
const zeroToNine = Array.from({ length: 10 }, (_, index) => index);
console.log(zeroToNine);  // 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Array.from 的强大之处

  • 第一个参数可以是任何有 length 属性的对象,或者可迭代对象(字符串、Set、Map 等)
  • 第二个参数是一个映射函数,类似于 map 方法,可以在转换的同时处理每个元素

2.5 展开运算符(ES6)

展开运算符 ... 可以把一个数组"展开"成单独的元素,也可以用来复制数组。

// 1. 复制数组
const original = [1, 2, 3];
const copy = [...original];
console.log(copy);      // [1, 2, 3]
console.log(copy === original);  // false —— 这是两个不同的数组

// 2. 合并多个数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];
const combined = [...arr1, ...arr2, ...arr3];
console.log(combined);  // [1, 2, 3, 4, 5, 6]

// 3. 在数组中添加新元素
const numbers = [2, 3, 4];
const withMore = [1, ...numbers, 5];
console.log(withMore);  // [1, 2, 3, 4, 5]

// 4. 把字符串转成数组(和 Array.from 效果一样)
const chars = [...'hello'];
console.log(chars);     // ['h', 'e', 'l', 'l', 'o']

展开运算符的优势

  • 语法简洁,可读性好
  • 创建的是数组的浅拷贝,不会影响原数组
  • 在函数参数中也很常用

第三部分:访问和修改数组元素

3.1 通过索引访问元素

数组的索引从 0 开始,使用中括号 [] 来访问。

const colors = ['红色', '橙色', '黄色', '绿色', '蓝色'];

// 访问元素
console.log(colors[0]);  // '红色' —— 第一个元素
console.log(colors[2]);  // '黄色' —— 第三个元素

// 访问最后一个元素
console.log(colors[colors.length - 1]);  // '蓝色'
// 解释:length 是 5,最后一个元素的索引是 4,所以用 length - 1

// 访问不存在的索引会返回 undefined
console.log(colors[100]);  // undefined
console.log(colors[-1]);   // undefined(负数索引在普通数组中无效)

3.2 at() 方法(ES2022)

at() 是较新的方法,它支持负数索引,让访问末尾元素更方便。

const colors = ['红色', '橙色', '黄色', '绿色', '蓝色'];

// 正数索引(和普通索引一样)
console.log(colors.at(0));   // '红色'
console.log(colors.at(2));   // '黄色'

// 负数索引(从末尾开始计数)
console.log(colors.at(-1));  // '蓝色' —— 最后一个元素
console.log(colors.at(-2));  // '绿色' —— 倒数第二个元素
console.log(colors.at(-3));  // '黄色' —— 倒数第三个元素

// 对比传统方式
// 传统方式获取最后一个元素
const last = colors[colors.length - 1];
// 使用 at() 获取最后一个元素
const lastWithAt = colors.at(-1);
// 显然 at(-1) 更简洁易懂

3.3 修改数组元素

const scores = [85, 90, 78, 92];

// 修改指定位置的元素
scores[1] = 95;
console.log(scores);  // [85, 95, 78, 92]

// 通过索引添加新元素
scores[4] = 88;
console.log(scores);  // [85, 95, 78, 92, 88]

// 如果跳过一个索引添加,中间会出现空位(不推荐这样做)
scores[10] = 100;
console.log(scores);  // [85, 95, 78, 92, 88, empty × 5, 100]
// 索引 5 到 9 都是空的,这被称为"稀疏数组"

3.4 length 属性

length 属性表示数组的长度(元素个数),它是自动维护的。

const fruits = ['苹果', '香蕉', '橙子'];
console.log(fruits.length);  // 3

// 添加元素后,length 自动增加
fruits.push('葡萄');
console.log(fruits.length);  // 4

// 手动修改 length 可以截断数组
fruits.length = 2;
console.log(fruits);  // ['苹果', '香蕉'] —— 后面的元素被删除了

// 把 length 设为 0 可以快速清空数组
fruits.length = 0;
console.log(fruits);  // [] —— 空数组

第四部分:添加和删除元素的方法

数组的强大之处在于可以动态地添加和删除元素。JavaScript 提供了多种方法来完成这些操作。

4.1 尾部操作:push 和 pop

想象数组是一个栈(stack),你只能从顶部放入或取出物品。

const stack = [];

// push —— 在数组末尾添加一个或多个元素
stack.push(1);
console.log(stack);  // [1]

stack.push(2, 3);    // 可以同时添加多个
console.log(stack);  // [1, 2, 3]

// push 返回添加元素后的数组长度
const newLength = stack.push(4);
console.log(newLength);  // 4
console.log(stack);      // [1, 2, 3, 4]

// pop —— 删除并返回数组的最后一个元素
const lastItem = stack.pop();
console.log(lastItem);   // 4
console.log(stack);      // [1, 2, 3]

// 对空数组 pop 返回 undefined
const empty = [];
console.log(empty.pop());  // undefined

使用场景

  • push:向列表末尾添加新项(如添加新消息到聊天记录)
  • pop:实现"撤销"功能,或处理栈结构的数据

4.2 头部操作:unshift 和 shift

这两个方法和 push/pop 类似,但操作的是数组的开头。

const queue = ['张三', '李四'];

// unshift —— 在数组开头添加一个或多个元素
queue.unshift('王五');
console.log(queue);  // ['王五', '张三', '李四']

queue.unshift('赵六', '孙七');  // 添加多个
console.log(queue);  // ['赵六', '孙七', '王五', '张三', '李四']

// shift —— 删除并返回数组的第一个元素
const first = queue.shift();
console.log(first);   // '赵六'
console.log(queue);   // ['孙七', '王五', '张三', '李四']

性能提示unshiftshiftpushpop 慢,因为它们需要移动数组中所有的现有元素。如果频繁在头部操作,考虑使用其他数据结构(如链表)。

4.3 万能方法:splice

splice 是数组中最强大的方法,可以添加、删除或替换元素。它的名字来源于"拼接"的意思。

const animals = ['狗', '猫', '鸟', '鱼'];

// 语法:array.splice(start, deleteCount, item1, item2, ...)
// start: 开始操作的索引位置
// deleteCount: 要删除的元素数量
// item1, item2, ...: 要添加的新元素(可选)

// 1. 删除元素
const removed = animals.splice(1, 2);
// 从索引1开始,删除2个元素
console.log(removed);   // ['猫', '鸟'] —— 被删除的元素
console.log(animals);   // ['狗', '鱼'] —— 剩下的元素

// 2. 添加元素(不删除)
animals.splice(1, 0, '兔子', '仓鼠');
// 从索引1开始,删除0个元素,添加'兔子'和'仓鼠'
console.log(animals);   // ['狗', '兔子', '仓鼠', '鱼']

// 3. 替换元素
animals.splice(1, 2, '龙猫');
// 从索引1开始,删除2个元素,添加'龙猫'
console.log(animals);   // ['狗', '龙猫', '鱼']

splice 的返回值:始终是一个包含被删除元素的数组(如果没有删除元素,返回空数组)。

4.4 截取数组:slice

slice 用于提取数组的一部分,不会改变原数组,而是返回一个新数组。

const fruits = ['苹果', '香蕉', '橙子', '葡萄', '西瓜'];

// 语法:array.slice(start, end)
// start: 开始索引(包含)
// end: 结束索引(不包含),可选,默认到数组末尾

// 提取索引 1 到 3(不包括3)的元素
const citrus = fruits.slice(1, 3);
console.log(citrus);   // ['香蕉', '橙子']
console.log(fruits);   // 原数组不变:['苹果', '香蕉', '橙子', '葡萄', '西瓜']

// 只传一个参数,从该索引提取到末尾
const lastThree = fruits.slice(2);
console.log(lastThree);  // ['橙子', '葡萄', '西瓜']

// 使用负数索引
const lastTwo = fruits.slice(-2);
console.log(lastTwo);    // ['葡萄', '西瓜']

// 复制整个数组
const copy = fruits.slice();
console.log(copy);       // 原数组的完整副本

slice vs splice

  • slice:不修改原数组,返回新数组,用于提取
  • splice:修改原数组,返回被删除的元素,用于增删改

4.5 填充数组:fill

fill 方法用一个固定值填充数组中从起始索引到终止索引内的全部元素。

// 创建长度为5的数组,全部填充为0
const zeros = new Array(5).fill(0);
console.log(zeros);  // [0, 0, 0, 0, 0]

// 部分填充
const arr = [1, 2, 3, 4, 5];
arr.fill('x', 1, 3);
// 从索引1开始,到索引3结束(不包括3),填充'x'
console.log(arr);    // [1, 'x', 'x', 4, 5]

// 实用技巧:重置数组
const board = ['X', 'O', 'X', ' ', 'O', ' ', ' ', ' ', 'X'];  // 井字棋棋盘
board.fill(' ');  // 清空棋盘
console.log(board);  // [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']

第五部分:遍历数组的方法

遍历(iteration)是指依次访问数组中的每个元素。JavaScript 提供了多种遍历方式,各有适用场景。

5.1 for 循环(传统方式)

这是最基础的遍历方式,在所有编程语言中都类似。

const scores = [85, 90, 78, 92, 88];

// 基础写法
for (let i = 0; i < scores.length; i++) {
    console.log(`${i + 1} 个学生的成绩是:${scores[i]}`);
}
// 输出:
// 第 1 个学生的成绩是:85
// 第 2 个学生的成绩是:90
// ...

// 优化写法:缓存 length 属性
for (let i = 0, len = scores.length; i < len; i++) {
    console.log(scores[i]);
}
// 这样只需要获取一次 length,对于大数组性能更好

// 倒序遍历
for (let i = scores.length - 1; i >= 0; i--) {
    console.log(scores[i]);
}

for 循环的优势

  • 性能最好
  • 可以控制遍历的步长(如每次跳过一个元素)
  • 可以使用 break 提前退出,使用 continue 跳过当前迭代

5.2 for…of 循环(ES6,推荐)

这是 ES6 引入的新语法,专门用于遍历可迭代对象(包括数组)。

const colors = ['红色', '绿色', '蓝色'];

// 直接获取每个元素的值
for (const color of colors) {
    console.log(color);
}
// 输出:红色、绿色、蓝色

// 如果需要索引,可以使用 entries()
for (const [index, color] of colors.entries()) {
    console.log(`${index}: ${color}`);
}
// 输出:
// 0: 红色
// 1: 绿色
// 2: 蓝色

for…of 的优势

  • 语法简洁,不需要管理索引
  • 可以直接获取元素值
  • 支持 breakcontinue

5.3 forEach 方法

forEach 是数组的原生方法,为每个元素执行一次提供的函数。

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

numbers.forEach(function(number, index, array) {
    console.log(`索引 ${index} 的值是 ${number}`);
});

// 使用箭头函数更简洁
numbers.forEach((number, index) => {
    console.log(`${index}: ${number}`);
});

// 如果只关心值,可以省略其他参数
numbers.forEach(num => console.log(num * 2));

forEach 的特点

  • 不能中途退出(不能使用 breakreturn 跳出整个循环)
  • 无法通过 return 改变循环行为
  • 适合对数组每个元素执行相同操作,且不需要中断的场景

5.4 对比三种遍历方式

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

// 场景1:需要提前退出 —— 用 for 或 for...of
for (const num of arr) {
    if (num > 3) break;  // 找到大于3的就停止
    console.log(num);    // 输出:1, 2, 3
}

// 场景2:只需要对每个元素执行操作 —— 用 forEach
arr.forEach(num => console.log(num * 2));

// 场景3:需要复杂控制(如每隔一个元素处理一次)—— 用 for
for (let i = 0; i < arr.length; i += 2) {
    console.log(arr[i]);  // 输出:1, 3, 5
}

第六部分:数组转换和排序

6.1 数组转字符串

const fruits = ['苹果', '香蕉', '橙子'];

// toString —— 默认用逗号连接
console.log(fruits.toString());  // "苹果,香蕉,橙子"

// join —— 可以指定分隔符
console.log(fruits.join());       // "苹果,香蕉,橙子"(默认逗号)
console.log(fruits.join(' - '));  // "苹果 - 香蕉 - 橙子"
console.log(fruits.join(''));     // "苹果香蕉橙子"(无分隔符)
console.log(fruits.join(' | '));  // "苹果 | 香蕉 | 橙子"

// 实用技巧:生成 HTML 列表
const html = '<li>' + fruits.join('</li><li>') + '</li>';
console.log(html);  // "<li>苹果</li><li>香蕉</li><li>橙子</li>"

6.2 数组排序

sort 方法

sort 方法对数组元素进行排序,会改变原数组

const letters = ['b', 'a', 'd', 'c'];
letters.sort();
console.log(letters);  // ['a', 'b', 'c', 'd'] —— 字母排序正确

// 但是数字排序有陷阱!
const numbers = [10, 2, 30, 1];
numbers.sort();
console.log(numbers);  // [1, 10, 2, 30] —— 咦?这不是我们想要的结果!

为什么数字排序不对?

默认情况下,sort 把元素转换成字符串,然后按 Unicode 编码排序。所以 “10” 排在 “2” 前面,因为 “1” 的 Unicode 编码小于 “2”。

正确的数字排序方式

const numbers = [10, 2, 30, 1];

// 升序排列(从小到大)
numbers.sort((a, b) => a - b);
console.log(numbers);  // [1, 2, 10, 30]

// 降序排列(从大到小)
numbers.sort((a, b) => b - a);
console.log(numbers);  // [30, 10, 2, 1]

比较函数的解释

  • (a, b) => a - b:如果结果小于 0,a 排在 b 前面(升序)
  • (a, b) => b - a:如果结果大于 0,a 排在 b 后面(降序)
对象数组排序
const students = [
    { name: '张三', score: 85 },
    { name: '李四', score: 92 },
    { name: '王五', score: 78 }
];

// 按成绩升序
students.sort((a, b) => a.score - b.score);
console.log(students);
// [{name: '王五', score: 78}, {name: '张三', score: 85}, {name: '李四', score: 92}]

// 按姓名排序(字符串比较)
students.sort((a, b) => a.name.localeCompare(b.name));
// localeCompare 是字符串的比较方法,适合中文排序
reverse 方法

reverse 方法反转数组中元素的顺序。

const arr = [1, 2, 3, 4, 5];
arr.reverse();
console.log(arr);  // [5, 4, 3, 2, 1]
ES2023 新方法(不改变原数组)
const arr = [3, 1, 4, 1, 5];

// toSorted —— 返回排序后的新数组,原数组不变
const sorted = arr.toSorted((a, b) => a - b);
console.log(sorted);  // [1, 1, 3, 4, 5]
console.log(arr);     // [3, 1, 4, 1, 5] —— 原数组没变

// toReversed —— 返回反转后的新数组
const reversed = arr.toReversed();
console.log(reversed);  // [5, 1, 4, 1, 3]
console.log(arr);       // [3, 1, 4, 1, 5] —— 原数组没变

6.3 合并数组

const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];

// concat 方法
const combined = arr1.concat(arr2, arr3);
console.log(combined);  // [1, 2, 3, 4, 5, 6]
console.log(arr1);      // [1, 2] —— 原数组不变

// 展开运算符(ES6,更推荐)
const combined2 = [...arr1, ...arr2, ...arr3];
console.log(combined2);  // [1, 2, 3, 4, 5, 6]

// 还可以在合并时添加新元素
const withExtra = [...arr1, '中间', ...arr2];
console.log(withExtra);  // [1, 2, '中间', 3, 4]

第七部分:搜索和查找元素

7.1 基础查找方法

const fruits = ['苹果', '香蕉', '橙子', '香蕉', '葡萄'];

// indexOf —— 查找元素第一次出现的索引
console.log(fruits.indexOf('香蕉'));      // 1
console.log(fruits.indexOf('西瓜'));      // -1(不存在)
console.log(fruits.indexOf('香蕉', 2));   // 3(从索引2开始查找)

// lastIndexOf —— 查找元素最后一次出现的索引
console.log(fruits.lastIndexOf('香蕉'));  // 3

// includes —— 判断是否包含某元素(ES2016)
console.log(fruits.includes('橙子'));     // true
console.log(fruits.includes('西瓜'));     // false

// includes 可以判断 NaN,indexOf 不行
console.log([NaN].includes(NaN));         // true
console.log([NaN].indexOf(NaN));          // -1(找不到)

7.2 条件查找方法

当需要根据条件查找,而不是精确匹配时,使用这些方法:

const students = [
    { name: '张三', age: 20, passed: true },
    { name: '李四', age: 22, passed: false },
    { name: '王五', age: 21, passed: true }
];

// find —— 返回第一个符合条件的元素
const firstPassed = students.find(s => s.passed);
console.log(firstPassed);  // { name: '张三', age: 20, passed: true }

const adult = students.find(s => s.age >= 21);
console.log(adult);  // { name: '李四', age: 22, passed: false }

// findIndex —— 返回第一个符合条件的索引
const adultIndex = students.findIndex(s => s.age >= 21);
console.log(adultIndex);  // 1

// findLast / findLastIndex —— 从后向前查找(ES2023)
const lastPassed = students.findLast(s => s.passed);
console.log(lastPassed);  // { name: '王五', age: 21, passed: true }

// some —— 检查是否有至少一个元素符合条件
const hasFailed = students.some(s => !s.passed);
console.log(hasFailed);  // true(有挂科的学生)

// every —— 检查是否所有元素都符合条件
const allAdults = students.every(s => s.age >= 18);
console.log(allAdults);  // true(都是成年人)

第八部分:高阶函数(重点掌握)

高阶函数是指接受函数作为参数或返回函数的函数。数组的三个高阶函数 mapfilterreduce 是现代 JavaScript 开发的核心工具,务必熟练掌握。

8.1 map —— 映射转换

map 方法创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。

通俗理解:把数组中的每个元素都"映射"成另一个值,就像给每个人都拍一张照片。

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

// 把每个数字翻倍
const doubled = numbers.map(num => num * 2);
console.log(doubled);   // [2, 4, 6, 8, 10]
console.log(numbers);   // [1, 2, 3, 4, 5] —— 原数组不变

// 把每个数字变成它的平方
const squared = numbers.map(num => num ** 2);
console.log(squared);   // [1, 4, 9, 16, 25]

// 实际应用:提取对象数组中的某个属性
const users = [
    { id: 1, name: '张三', email: 'zhang@example.com' },
    { id: 2, name: '李四', email: 'li@example.com' },
    { id: 3, name: '王五', email: 'wang@example.com' }
];

const names = users.map(user => user.name);
console.log(names);  // ['张三', '李四', '王五']

const userInfo = users.map(user => `${user.name} (${user.email})`);
console.log(userInfo);  // ['张三 (zhang@example.com)', '李四 (li@example.com)', ...]

// map 接收三个参数:当前元素、索引、原数组
const withIndex = numbers.map((num, index) => `${index}: ${num}`);
console.log(withIndex);  // ['0: 1', '1: 2', '2: 3', '3: 4', '4: 5']

8.2 filter —— 过滤筛选

filter 方法创建一个新数组,包含所有通过测试的元素。

通俗理解:就像用筛子过滤,只保留符合条件的元素。

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

// 筛选出偶数
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens);   // [2, 4, 6, 8, 10]

// 筛选出大于5的数字
const bigOnes = numbers.filter(num => num > 5);
console.log(bigOnes);  // [6, 7, 8, 9, 10]

// 实际应用:筛选活跃用户
const users = [
    { name: '张三', active: true },
    { name: '李四', active: false },
    { name: '王五', active: true }
];

const activeUsers = users.filter(user => user.active);
console.log(activeUsers);  // [{name: '张三', active: true}, {name: '王五', active: true}]

// 筛选成年用户
const people = [
    { name: '小明', age: 16 },
    { name: '小红', age: 20 },
    { name: '小刚', age: 25 }
];
const adults = people.filter(person => person.age >= 18);
console.log(adults);  // [{name: '小红', age: 20}, {name: '小刚', age: 25}]

// 实用技巧:数组去重(结合 indexOf)
const duplicates = [1, 2, 2, 3, 3, 3, 4];
const unique = duplicates.filter((item, index, arr) => arr.indexOf(item) === index);
console.log(unique);  // [1, 2, 3, 4]
// 原理:indexOf 总是返回第一次出现的索引,如果当前索引不等于第一次出现的索引,说明是重复项

8.3 reduce —— 归约汇总

reduce 方法对数组中的每个元素执行一个 reducer 函数,最终汇总为一个值。

通俗理解:把数组中的所有元素"归约"成一个值,就像把一堆东西打包成一个包裹。

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

// 基本语法:array.reduce((累加器, 当前值) => 新的累加器, 初始值)

// 求和
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
// 执行过程:
// 初始: acc = 0
// 第1次: acc = 0 + 1 = 1
// 第2次: acc = 1 + 2 = 3
// 第3次: acc = 3 + 3 = 6
// 第4次: acc = 6 + 4 = 10
// 第5次: acc = 10 + 5 = 15
console.log(sum);  // 15

// 求积
const product = numbers.reduce((acc, curr) => acc * curr, 1);
console.log(product);  // 120 (1*2*3*4*5)

// 找最大值
const max = numbers.reduce((acc, curr) => Math.max(acc, curr));
console.log(max);  // 5

// 实际应用:分组
const people = [
    { name: '张三', department: '技术部' },
    { name: '李四', department: '销售部' },
    { name: '王五', department: '技术部' },
    { name: '赵六', department: '销售部' }
];

const grouped = people.reduce((acc, person) => {
    const dept = person.department;
    // 如果该部门还不存在,创建一个空数组
    if (!acc[dept]) {
        acc[dept] = [];
    }
    // 把当前人员添加到对应部门
    acc[dept].push(person);
    return acc;
}, {});

console.log(grouped);
// {
//   '技术部': [{name: '张三', ...}, {name: '王五', ...}],
//   '销售部': [{name: '李四', ...}, {name: '赵六', ...}]
// }

// 实际应用:统计出现次数
const fruits = ['苹果', '香蕉', '苹果', '橙子', '香蕉', '苹果'];
const count = fruits.reduce((acc, fruit) => {
    acc[fruit] = (acc[fruit] || 0) + 1;
    return acc;
}, {});
console.log(count);  // { '苹果': 3, '香蕉': 2, '橙子': 1 }

8.4 链式调用

mapfilterreduce 都返回数组(reduce 返回汇总值),所以可以链式调用:

const students = [
    { name: '张三', score: 85 },
    { name: '李四', score: 92 },
    { name: '王五', score: 78 },
    { name: '赵六', score: 95 }
];

// 需求:找出成绩大于80分的学生,提取他们的名字,然后用逗号连接
const result = students
    .filter(s => s.score > 80)     // 筛选高分学生
    .map(s => s.name)               // 提取名字
    .join('、');                    // 连接成字符串

console.log(result);  // "张三、李四、赵六"

// 更复杂的例子:计算平均分
const average = students
    .filter(s => s.score >= 60)     // 只算及格的学生
    .map(s => s.score)              // 提取分数
    .reduce((sum, score, _, arr) => sum + score / arr.length, 0);

console.log(average);  // 及格学生的平均分

第九部分:数组扁平化

9.1 flat 方法

flat 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

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

// 默认只扁平化一层
console.log(nested.flat());     // [1, 2, 3, [4, 5], 6]

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

// 完全扁平化(不管嵌套多深)
const deep = [1, [2, [3, [4, [5]]]]];
console.log(deep.flat(Infinity));  // [1, 2, 3, 4, 5]

// 自动去除空项
const sparse = [1, , 2, , 3];
console.log(sparse.flat());     // [1, 2, 3]

9.2 flatMap 方法

flatMap 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它等价于 map 后跟着深度为 1 的 flat

const sentences = ['Hello world', 'Good morning', 'See you'];

// 先 map 分割成单词数组,再 flat 扁平化
const words = sentences.flatMap(s => s.split(' '));
console.log(words);  // ['Hello', 'world', 'Good', 'morning', 'See', 'you']

// 对比分开写
const words2 = sentences.map(s => s.split(' ')).flat();
// 结果相同,但 flatMap 更高效

// 实用技巧:过滤并转换
const numbers = [1, 2, 3, 4, 5, 6];
// 只保留偶数,并把它们翻倍
const result = numbers.flatMap(n => n % 2 === 0 ? [n * 2] : []);
console.log(result);  // [4, 8, 12]
// 原理:返回空数组的元素会被 flat 掉,相当于过滤掉了

第十部分:实用技巧和最佳实践

10.1 快速清空数组

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

// 方法1:设置 length 为 0(最快)
arr.length = 0;

// 方法2:使用 splice
arr.splice(0, arr.length);

// 方法3:重新赋值为空数组(如果不需要保持引用)
// arr = [];  // 这只在 arr 用 let 声明时有效

10.2 数组去重

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

// 方法1:使用 Set(最简洁,推荐)
const unique1 = [...new Set(duplicates)];
console.log(unique1);  // [1, 2, 3, 4]

// 方法2:使用 filter
const unique2 = duplicates.filter((item, index, arr) => 
    arr.indexOf(item) === index
);

// 方法3:使用 reduce
const unique3 = duplicates.reduce((acc, curr) => 
    acc.includes(curr) ? acc : [...acc, curr]
, []);

10.3 判断数组

// 正确的方式
Array.isArray([]);           // true
Array.isArray({});           // false
Array.isArray('string');     // false
Array.isArray(null);         // false

// 错误的方式(不要这样用)
typeof [];                   // 'object' —— 数组也是对象!
[] instanceof Array;         // true,但在跨 iframe 时可能失效

10.4 随机打乱数组(Fisher-Yates 算法)

function shuffle(array) {
    // 复制数组,避免修改原数组
    const arr = [...array];
    
    for (let i = arr.length - 1; i > 0; i--) {
        // 生成 0 到 i 的随机索引
        const j = Math.floor(Math.random() * (i + 1));
        // 交换位置
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    
    return arr;
}

const cards = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
const shuffled = shuffle(cards);
console.log(shuffled);  // 随机顺序的牌组

10.5 数组分块

function chunk(array, size) {
    return Array.from({ length: Math.ceil(array.length / size) }, 
        (_, index) => array.slice(index * size, index * size + size)
    );
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(chunk(numbers, 3));  // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
console.log(chunk(numbers, 4));  // [[1, 2, 3, 4], [5, 6, 7, 8], [9]]

10.6 获取数组交集、差集

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

// 交集(共同拥有的)
const intersection = arr1.filter(x => arr2.includes(x));
console.log(intersection);  // [4, 5]

// 差集(arr1 有但 arr2 没有的)
const difference = arr1.filter(x => !arr2.includes(x));
console.log(difference);  // [1, 2, 3]

// 并集(所有的,去重)
const union = [...new Set([...arr1, ...arr2])];
console.log(union);  // [1, 2, 3, 4, 5, 6, 7, 8]

总结

在这篇文章中,我们系统地学习了 JavaScript 数组的各个方面:

核心知识点回顾

类别 关键方法 用途
创建数组 [], Array.of, Array.from, ... 初始化数据
增删元素 push/pop, unshift/shift, splice 修改数组内容
访问元素 [], at() 读取数据
遍历数组 for, for...of, forEach 处理每个元素
转换数组 map, filter, flat 创建新数组
汇总数据 reduce 归约为单个值
查找元素 find, indexOf, includes 定位数据
排序 sort, reverse 调整顺序

给初学者的建议

  1. 从基础开始:先掌握数组字面量创建、索引访问、push/pop 这些基础操作
  2. 重点练习高阶函数mapfilterreduce 是现代 JavaScript 的核心,务必熟练掌握
  3. 多写代码:数组方法很多,只有通过实践才能真正记住和理解
  4. 注意不变性:优先使用不改变原数组的方法(如 map 而不是 forEach 修改),这样代码更易于理解和维护
  5. 查阅文档:遇到不确定的方法,及时查阅 MDN 文档

练习建议

  1. 尝试用 mapfilterreduce 重写你之前用 for 循环写的代码
  2. 实现一个简单的待办事项列表,练习增删改查操作
  3. 处理一些真实数据(如从 API 获取的用户列表),进行筛选、排序、分组等操作

希望这篇文章能帮助你全面掌握 JavaScript 数组!如果有任何疑问,欢迎继续探讨。

更多推荐