本文面向正在学习 JavaScript 的同学,从“数组到底是什么”出发,深入理解数组在内存中的本质、创建方式、动态扩容机制、遍历性能差异,以及它与对象、链表的区别。阅读后,你不仅能熟练使用数组,还能理解其设计背后的逻辑。
一、数据结构的分类
在计算机中,数据结构大致分为两类:线性结构与非线性结构。
1. 线性数据结构
元素之间是一对一的线性关系,常见类型包括:
- 数组(Array) :最常用、存储连续、访问高效。
- 栈(Stack) :先进后出(FILO)。
- 队列(Queue) :先进先出(FIFO)。
- 链表(Linked List) :节点离散,通过指针相连。
2. 非线性数据结构
元素之间是一对多或多对多的关系,如:
- 树(Tree) :如二叉树、平衡树。
- 图(Graph) :节点连接关系更复杂。
本文将重点讲解最常见的线性结构——数组。
二、数组的本质:连续内存与索引访问
从底层角度看,数组是一段连续的内存空间,每个元素占据固定大小的存储单元,通过下标偏移即可直接访问。
const arr = [1, 2, 3, 4, 5, 6];
执行这行代码后:
- 数组元素
[1, 2, 3, 4, 5, 6]存放在堆内存中; - 栈内存中保存着一个引用地址
arr,指向堆内的实际数据。
因此数组是引用类型,复制时传递的是地址,而非值。
三、数组的创建方式
1. 字面量创建(最常见)
const arr = [1, 2, 3, 4, 5, 6];
这种方式最直观、最常用,推荐在绝大多数情况下使用。
2. 构造函数创建
const arr1 = new Array(6); // 创建一个长度为 6 的空数组
const arr2 = new Array(6).fill(0); // 创建并用 0 填充
new Array(6) 仅仅是分配了一个长度为 6 的空槽位,并没有真正初始化值;而 fill(0) 则会为每个位置赋初始值。
3. 空数组 + 动态扩容
const arr = [];
arr.push(1);
arr.push(2);
JavaScript 的数组可以动态扩容。当空间不足时,底层会申请一块更大的连续内存,并将原数组内容复制过去。这一过程被称为“搬家”。
动态扩容带来了灵活性,但频繁扩容会造成额外的性能开销。因此:
- 若数据量较小、随机访问频繁,数组是高效的选择;
- 若频繁插入或删除元素,链表更合适。
四、数组的访问与时间复杂度
数组支持通过下标直接访问任意元素:
arr[index]; // O(1)
由于数组内存空间是连续的,CPU 可以直接通过基址加偏移量定位到具体元素,访问效率极高。这是数组的核心优势。
五、数组的遍历方式与性能对比
遍历是数组的基础操作,不同遍历方式在语义、可读性与性能上各有优劣。
1. 计数循环(for)
const arr = new Array(6).fill(0);
const len = arr.length;
for (let i = 0; i < len; i++) {
console.log(arr[i]);
}
特点:
- 性能最佳;
- 没有函数调用栈的开销;
- CPU 对这种循环结构高度优化。
一般在性能要求高的场景(如算法题、大量数据计算)中使用。
2. forEach
const arr = [1, 2, 3, 4, 5, 6];
arr.forEach((item, index) => {
console.log(item, index);
});
forEach 语义清晰,可读性强,但有几点需要注意:
- 无法使用
break或return中断循环; - 每次调用函数会产生额外的执行开销;
- 性能略低于传统
for。
适用于逻辑简单、以可读性为主的业务场景。
3. map(加工生成新数组)
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.map(item => item + 1);
console.log(newArr);
区别在于:
forEach仅执行函数,不返回结果;map会返回一个经过加工的新数组。
适用于需要对数组进行转换、生成新数组的场景。例如将一组数字全部加一或从对象中提取指定属性。
4. for...of(ES6 推荐写法)
const arr = [1, 2, 3, 4, 5, 6];
for (let item of arr) {
console.log(item);
}
for...of 是 ES6 提供的语法糖,内部基于迭代器实现,语义上表示“遍历可迭代对象的值”。
优点:
- 语法简洁;
- 可读性强;
- 可以在异步循环(如 async/await)中配合使用。
缺点:
- 无法直接获取下标(可用
entries()方法间接实现)。
5. for...in(不推荐用于数组)
const arr = [1, 2, 3, 4, 5, 6];
for (let key in arr) {
console.log(key, arr[key]);
}
for...in 是为对象属性设计的。虽然数组也是对象,但使用 for...in 遍历数组会产生以下问题:
- 遍历顺序不一定与索引顺序一致;
- 可能遍历出原型链上的属性;
- 性能较差。
因此,for...in 只适用于普通对象,不推荐用于数组。
六、作用域与闭包:循环中的陷阱
一个常见的例子:
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
输出结果为 0 到 9。
因为 let 声明的变量具有块级作用域,每次循环都会生成独立的词法环境。
若改为 var,则输出全是 10,因为 var 没有块级作用域,所有回调共享同一个 i。
七、总结
-
数组的本质是连续的内存空间,通过下标实现 O(1) 访问。
-
动态扩容带来灵活性,但会有复制开销。
-
不同遍历方式各有优缺点:
for:性能最优;forEach:语义清晰;map:适合生成新数组;for...of:语法现代;for...in:仅适合对象。
-
作用域与闭包是遍历异步时常见的陷阱,应注意
var与let的区别。
掌握这些内容后,你就能从原理层面理解数组的运行方式,不再只是“会用”,而是“知其所以然”。这也是理解更复杂数据结构(如链表、树、哈希表)的基础。