JavaScript学习笔记:9.数组
上一篇咱们驯服了JS的“时间管家”(日期与时间),这一篇来解锁开发中出场率最高的“数据容器”——数组(Array)。如果把JS的数据类型比作生活中的收纳工具,那数组就是“带格子的万能收纳箱”:既能按顺序装下一堆数据,又能批量操作、筛选、改造里面的内容,不管是展示列表数据、处理接口返回结果,还是操作DOM元素,都离不开它。
新手常栽在数组的“陷阱”里:比如用sort()排序数字却得到乱序结果,用forEach()想终止循环却发现行不通,或是把“类数组”当数组用导致方法报错。今天就用“收纳箱管理”的思路,把数组的本质、核心方法、实战技巧和避坑指南讲透,让你既能“装对东西”,又能“用好收纳箱”。
一、先搞懂:数组的本质——不是“真正的数组”?
在聊用法之前,先戳破一个颠覆认知的真相:JavaScript的数组不是传统意义上的“数组”。传统数组(比如Java的数组)是“连续的内存空间”,元素类型固定、长度不可变;但JS数组是“对象伪装的”——它用数字作为属性名(索引),用length属性记录元素个数,本质是“键值对集合”。
这个特性带来两个核心优势:
- 动态长度:收纳箱能自动扩容/缩容,不用提前声明大小;
- 元素类型不限:可以同时装数字、字符串、对象甚至另一个数组(就像收纳箱里套小收纳箱)。
// JS数组:元素类型不限,动态长度
const mixArray = [10, "奶茶", { name: "张三" }, [1,2,3]];
console.log(mixArray.length); // 4(自动记录长度)
mixArray.push(true); // 新增元素,数组自动扩容
console.log(mixArray.length); // 5(长度变成5)
💡 虽然JS数组支持混装元素,但不推荐这么做——会降低代码可读性,也会影响遍历和操作效率,就像收纳箱里混装衣服和餐具,不方便整理。
二、创建数组:四种姿势,选对不踩坑
创建数组就像“买收纳箱”,有不同的渠道,有的方便快捷,有的适合特殊场景。重点记前两种,后两种是进阶补充。
1. 字面量方式:开发首选,简洁高效
用[]包裹元素创建数组,是最常用的方式,就像“现成的收纳箱”,拿过来直接装东西。
// 空数组
const emptyArr = [];
// 有元素的数组
const fruitArr = ["苹果", "香蕉", "橙子"];
// 嵌套数组(收纳箱套收纳箱)
const nestedArr = [1, [2, 3], [4, [5]]];
2. Array构造函数:小心“单数字”陷阱
用new Array()创建数组,看似正规,但有个巨坑——当只传一个数字参数时,它不是创建“包含这个数字的数组”,而是创建“长度为这个数字的空数组”。
// 正面例子1:传多个参数(正常创建数组)
const arr1 = new Array(10, 20, 30);
console.log(arr1); // [10, 20, 30](正确)
// 正面例子2:传非数字参数(正常创建数组)
const arr2 = new Array("苹果");
console.log(arr2); // ["苹果"](正确)
// 反面例子:单数字参数(坑!)
const arr3 = new Array(3);
console.log(arr3); // [empty × 3](长度为3的空数组,不是[3])
console.log(arr3[0]); // undefined(没有实际元素)
避坑:除非需要创建“指定长度的空数组”,否则一律用字面量方式。
3. Array.of():解决构造函数的陷阱
ES6新增的Array.of(),专门解决Array构造函数的单数字问题——不管传几个参数、什么类型,都直接作为元素创建数组。
const arr1 = Array.of(3);
console.log(arr1); // [3](正确,不是空数组)
const arr2 = Array.of(10, 20, 30);
console.log(arr2); // [10, 20, 30](和构造函数一致)
const arr3 = Array.of("苹果", true, { name: "张三" });
console.log(arr3); // ["苹果", true, {name: "张三"}](支持多类型)
4. Array.from():把“类数组”变真数组
ES6新增的Array.from()是“转换大师”,能把“类数组对象”(比如DOM元素集合、arguments)和“可迭代对象”(比如Set、Map)转成真正的数组。这是实战中高频使用的技巧,比如操作页面上的多个按钮。
// 1. 类数组对象转数组(比如DOM元素集合)
const buttons = document.querySelectorAll("button"); // 类数组(NodeList)
const buttonArr = Array.from(buttons); // 转成真正的数组,就能用数组方法了
// 2. 字符串转数组
const str = "奶茶";
const strArr = Array.from(str);
console.log(strArr); // ["奶", "茶"]
// 3. 生成有序数组(比如1-10)
const numArr = Array.from({ length: 10 }, (_, index) => index + 1);
console.log(numArr); // [1,2,3,4,5,6,7,8,9,10]
三、数组的核心特性:三个必须记住的“规矩”
数组的用法之所以容易踩坑,是因为它有几个“反直觉”的特性,记住这三条规矩,能避开80%的基础错误。
1. 索引从0开始:和“收纳箱格子编号”一样
数组的索引(元素位置)是从0开始的,不是1——第一个元素是arr[0],第二个是arr[1],就像收纳箱的格子编号从0开始,不是1。这是新手最常犯的错,和之前日期的“月份从0开始”堪称“JS两大索引陷阱”。
const fruitArr = ["苹果", "香蕉", "橙子"];
console.log(fruitArr[0]); // "苹果"(第一个元素,正确)
console.log(fruitArr[1]); // "香蕉"(第二个元素,正确)
console.log(fruitArr[3]); // undefined(没有第四个元素,报错源)
2. length:不是“实际元素数”,是“最大索引+1”
数组的length属性不是“实际有多少个元素”,而是“最大索引+1”。这意味着:
- 手动修改
length会截断或扩容数组; - 空元素(稀疏数组)不影响
length。
const arr = [10, 20, 30];
console.log(arr.length); // 3(最大索引2+1)
// 1. 手动改小length:截断数组(丢失元素)
arr.length = 2;
console.log(arr); // [10, 20](30被删掉了)
// 2. 手动改大length:扩容数组(添加空元素)
arr.length = 5;
console.log(arr); // [10, 20, empty × 3](新增3个空元素)
// 3. 稀疏数组:length是最大索引+1,不管空元素
const sparseArr = [1, , 3]; // 索引1是空元素
console.log(sparseArr.length); // 3(最大索引2+1)
⚠️ 尽量别手动修改length——截断会丢失数据,扩容会产生空元素,都可能导致后续遍历出错。
3. 数组是“引用类型”:赋值传递的是“地址”
数组和对象一样,是引用类型——把数组赋值给新变量,传递的不是“数组副本”,而是“内存地址”。就像把收纳箱的“钥匙”给了别人,别人修改收纳箱里的东西,你看到的也会变。
// 反面例子:引用赋值,修改新数组会影响原数组
const arr1 = [10, 20, 30];
const arr2 = arr1; // 传递的是地址,不是副本
arr2[0] = 100; // 修改arr2
console.log(arr1); // [100, 20, 30](原数组也变了!)
// 正面例子:创建副本(避免影响原数组)
const arr3 = [...arr1]; // 扩展运算符创建副本
const arr4 = arr1.slice(); // slice方法创建副本
const arr5 = Array.from(arr1); // Array.from创建副本
arr3[0] = 200;
console.log(arr1); // [100, 20, 30](原数组不变)
四、数组核心方法:按“用途分类”记,效率翻倍
数组的方法有几十种,但高频实用的就十几款。按“用途分类”记忆,比死记硬背高效得多。核心分为:遍历/转换、修改原数组、查询/筛选、排序/拼接四大类。
1. 遍历/转换类:不改变原数组,“只读不写”
这类方法不会修改原数组,只会返回新结果,是“安全操作”,适合处理数据转换、批量处理元素。
| 方法 | 用途 | 核心特点 |
|---|---|---|
| forEach | 逐个遍历元素 | 无返回值,不能break/continue |
| map | 改造元素,返回新数组 | 新数组长度和原数组一致 |
| filter | 筛选符合条件的元素 | 新数组长度≤原数组 |
| reduce | 汇总元素,返回任意值 | 万能方法,可实现求和、转对象等 |
| flat | 扁平化数组 | 处理嵌套数组,默认深度1 |
| flatMap | 先map再flat(1) | 简化“改造+扁平化”流程 |
(1)forEach:逐个“清点”收纳箱(无返回值)
最基础的遍历方法,逐个执行元素的回调函数,就像“逐个清点收纳箱里的东西”。但它没有返回值(返回undefined),也不能用break/continue终止循环(除非抛异常,不推荐)。
const fruitArr = ["苹果", "香蕉", "橙子"];
// 遍历数组,打印每个元素和索引
fruitArr.forEach((item, index) => {
console.log(`索引${index}:${item}`);
});
// 输出:
// 索引0:苹果
// 索引1:香蕉
// 索引2:橙子
(2)map:“改造”收纳箱里的东西(返回新数组)
遍历数组,对每个元素做“改造”,返回包含改造后元素的新数组——就像把收纳箱里的苹果都洗干净,得到“洗干净的苹果数组”。核心:新数组长度和原数组一致。
const numArr = [1, 2, 3, 4];
// 把每个数字乘以2(改造元素)
const doubleArr = numArr.map(item => item * 2);
console.log(doubleArr); // [2, 4, 6, 8](新数组)
console.log(numArr); // [1,2,3,4](原数组不变)
// 实战场景:提取对象数组的某个属性
const userArr = [
{ name: "张三", age: 25 },
{ name: "李四", age: 30 }
];
const nameArr = userArr.map(user => user.name);
console.log(nameArr); // ["张三", "李四"](提取姓名数组)
(3)filter:“筛选”收纳箱里的东西(返回新数组)
遍历数组,筛选出“符合条件的元素”,返回新数组——就像从收纳箱里挑出“红色的东西”。核心:新数组长度≤原数组。
const numArr = [1, 2, 3, 4, 5, 6];
// 筛选出偶数
const evenArr = numArr.filter(item => item % 2 === 0);
console.log(evenArr); // [2, 4, 6](新数组)
// 实战场景:筛选成年用户
const userArr = [
{ name: "张三", age: 17 },
{ name: "李四", age: 30 },
{ name: "王五", age: 22 }
];
const adultArr = userArr.filter(user => user.age >= 18);
console.log(adultArr); // 包含李四、王五的数组
(4)reduce:“汇总”收纳箱里的东西(返回任意值)
数组的“万能方法”,可以实现求和、求最大值、数组转对象、扁平化数组等各种汇总操作——就像把收纳箱里的东西全部打包成一个“汇总包”。它接收两个核心参数:累加器(acc)和当前元素(cur)。
const numArr = [1, 2, 3, 4];
// 1. 求和(最常用)
const sum = numArr.reduce((acc, cur) => acc + cur, 0); // 初始值0
console.log(sum); // 10
// 2. 求最大值
const max = numArr.reduce((acc, cur) => Math.max(acc, cur), numArr[0]);
console.log(max); // 4
// 3. 数组转对象(实战常用)
const userArr = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const userObj = userArr.reduce((acc, cur) => {
acc[cur.id] = cur.name; // 以id为键,name为值
return acc;
}, {}); // 初始值是空对象
console.log(userObj); // {1: "张三", 2: "李四"}
2. 修改原数组类:“动手改造”收纳箱(慎用)
这类方法会直接修改原数组(产生“副作用”),就像“拆改收纳箱”,使用时要格外小心——如果需要保留原数组,一定要先创建副本。
| 方法 | 用途 | 核心特点 |
|---|---|---|
| push | 末尾添加元素 | 返回新长度 |
| pop | 末尾删除元素 | 返回删除的元素 |
| unshift | 开头添加元素 | 返回新长度,效率低 |
| shift | 开头删除元素 | 返回删除的元素,效率低 |
| splice | 删除/替换/插入元素 | 功能强大,修改原数组 |
| reverse | 颠倒数组顺序 | 原地反转,修改原数组 |
| sort | 排序数组 | 原地排序,默认按字符串排序 |
(1)push/pop:操作数组末尾(高效)
这两个方法操作数组末尾,就像“从收纳箱顶部放东西/拿东西”,效率很高(推荐使用)。
const arr = [10, 20];
// push:末尾添加元素,返回新长度
const newLen = arr.push(30, 40);
console.log(arr); // [10, 20, 30, 40]
console.log(newLen); // 4
// pop:末尾删除元素,返回删除的元素
const lastItem = arr.pop();
console.log(arr); // [10, 20, 30]
console.log(lastItem); // 40
(2)unshift/shift:操作数组开头(低效)
这两个方法操作数组开头,就像“从收纳箱底部放东西/拿东西”,需要移动所有元素,数据量越大效率越低(尽量少用)。
const arr = [10, 20];
// unshift:开头添加元素,返回新长度
const newLen = arr.unshift(-10, 0);
console.log(arr); // [-10, 0, 10, 20]
console.log(newLen); // 4
// shift:开头删除元素,返回删除的元素
const firstItem = arr.shift();
console.log(arr); // [0, 10, 20]
console.log(firstItem); // -10
(3)splice:数组的“瑞士军刀”(功能强大)
splice是最灵活的修改方法,能实现“删除、替换、插入”三种功能,语法:splice(起始索引, 删除个数, 插入的元素...)。
const arr = [1, 2, 3, 4, 5];
// 1. 删除:从索引1开始,删除2个元素
arr.splice(1, 2);
console.log(arr); // [1, 4, 5](删除了2、3)
// 2. 替换:从索引1开始,删除1个元素,插入"a"
arr.splice(1, 1, "a");
console.log(arr); // [1, "a", 5](把4换成了"a")
// 3. 插入:从索引2开始,删除0个元素,插入"b"、"c"
arr.splice(2, 0, "b", "c");
console.log(arr); // [1, "a", "b", "c", 5](插入了b、c)
(4)sort:排序的“坑王”(默认按字符串排序)
sort方法默认按“字符串Unicode编码”排序,不是按数字大小——这是新手最常踩的排序坑!必须传入自定义比较函数才能正确排序数字。
// 反面例子:数字排序(坑!)
const numArr = [10, 2, 30, 1];
numArr.sort();
console.log(numArr); // [1, 10, 2, 30](按字符串排序,乱序)
// 正面例子:传入比较函数(正确排序)
numArr.sort((a, b) => a - b); // 升序:a - b < 0 → a在前
console.log(numArr); // [1, 2, 10, 30](正确)
numArr.sort((a, b) => b - a); // 降序:b - a < 0 → b在前
console.log(numArr); // [30, 10, 2, 1](正确)
3. 查询/筛选类:快速找到“目标物品”
这类方法用于查找数组中的元素或索引,不用手动遍历,效率更高。
| 方法 | 用途 | 核心特点 |
|---|---|---|
| indexOf | 查找元素,返回第一个匹配索引 | 找不到返回-1 |
| lastIndexOf | 反向查找元素,返回最后一个匹配索引 | 找不到返回-1 |
| find | 查找符合条件的元素,返回第一个匹配元素 | 找不到返回undefined |
| findIndex | 查找符合条件的元素,返回第一个匹配索引 | 找不到返回-1 |
| findLast | 反向查找符合条件的元素,返回最后一个匹配元素 | 找不到返回undefined |
| findLastIndex | 反向查找符合条件的元素,返回最后一个匹配索引 | 找不到返回-1 |
| every | 判断所有元素是否符合条件 | 全符合返回true,否则false |
| some | 判断是否有元素符合条件 | 至少一个符合返回true |
| at | 按索引访问元素,支持负索引 | 负索引从末尾开始(-1是最后一个) |
const userArr = [
{ id: 1, name: "张三", age: 25 },
{ id: 2, name: "李四", age: 30 },
{ id: 3, name: "王五", age: 25 }
];
// 1. 查找name为"李四"的元素
const liSi = userArr.find(user => user.name === "李四");
console.log(liSi); // {id:2, name:"李四", age:30}
// 2. 查找age=25的最后一个元素的索引
const last25Index = userArr.findLastIndex(user => user.age === 25);
console.log(last25Index); // 2(王五的索引)
// 3. 判断是否所有用户都成年
const allAdult = userArr.every(user => user.age >= 18);
console.log(allAdult); // true
// 4. 用at访问负索引(最后一个元素)
const lastUser = userArr.at(-1);
console.log(lastUser); // {id:3, name:"王五", age:25}
4. 排序/拼接类:组合或整理“收纳箱”
这类方法用于数组的组合、扁平化或格式转换,大多不修改原数组。
| 方法 | 用途 | 核心特点 |
|---|---|---|
| concat | 连接多个数组,返回新数组 | 不修改原数组 |
| join | 把数组元素连接成字符串,返回字符串 | 不修改原数组 |
| flat | 扁平化数组,返回新数组 | 不修改原数组,默认深度1 |
| flatMap | 先map再flat(1),返回新数组 | 不修改原数组 |
// 1. concat:连接数组
const arr1 = [1, 2];
const arr2 = [3, 4];
const newArr = arr1.concat(arr2, [5, 6]);
console.log(newArr); // [1,2,3,4,5,6](原数组不变)
// 2. join:数组转字符串
const fruitArr = ["苹果", "香蕉", "橙子"];
const str = fruitArr.join("、");
console.log(str); // "苹果、香蕉、橙子"
// 3. flat:扁平化数组
const nestedArr = [1, [2, [3, 4]]];
const flat1 = nestedArr.flat(); // 深度1
console.log(flat1); // [1,2,[3,4]]
const flat2 = nestedArr.flat(2); // 深度2
console.log(flat2); // [1,2,3,4]
五、实战避坑:数组的“高频陷阱”汇总
1. 稀疏数组的坑:空槽≠undefined
稀疏数组(有空槽的数组)在迭代时会跳过空槽,但undefined会被遍历到——这是容易忽略的细节。
const sparseArr = [1, , 3]; // 空槽
const undefinedArr = [1, undefined, 3]; // undefined
// forEach遍历:空槽被跳过,undefined被遍历
sparseArr.forEach(item => console.log(item)); // 1、3
undefinedArr.forEach(item => console.log(item)); // 1、undefined、3
避坑:尽量避免创建稀疏数组,空值用undefined显式表示。
2. 类数组对象的坑:不是数组,没有数组方法
DOM集合(NodeList)、arguments等是“类数组”(有length和索引),但没有数组方法(如forEach),直接调用会报错。
// 反面例子:类数组直接调用数组方法(报错)
const buttons = document.querySelectorAll("button");
buttons.forEach(btn => btn.click()); // TypeError: buttons.forEach is not a function
// 正面例子:转成真正的数组
const buttonArr = Array.from(buttons);
buttonArr.forEach(btn => btn.click()); // 正确执行
3. 不要用for…in遍历数组
for...in会遍历数组的所有可枚举属性(包括自定义属性),而不只是元素,导致遍历结果异常。
const arr = [1, 2, 3];
arr.customProp = "自定义属性"; // 给数组加自定义属性
// 反面例子:for...in遍历(会包含自定义属性)
for (const key in arr) {
console.log(key); // 0、1、2、customProp(坑!)
}
// 正面例子:用for...of或forEach
for (const item of arr) {
console.log(item); // 1、2、3(正确)
}
六、总结:数组使用的“核心原则”
- 优先用字面量创建数组,避免Array构造函数的单数字陷阱;
- 操作数组时,区分“修改原数组”和“返回新数组”,需要保留原数组就先创建副本;
- 遍历数组用for…of、forEach,查询用find/findIndex,改造用map/filter,汇总用reduce;
- 数字排序必须传入比较函数,类数组转数组用Array.from;
- 避免稀疏数组、手动修改length、用for…in遍历数组。
数组是JS最核心的数据结构之一,吃透这些方法和避坑点,能大幅提升开发效率。下一篇笔记,我们会聊JS的“对象与原型”,解锁面向对象编程的基础。
JavaScript数组核心详解
346

被折叠的 条评论
为什么被折叠?



