📚在 JavaScript 的世界中,数组(Array)是最基础、最常用的数据结构之一。它既简单又强大,但其背后却隐藏着丰富的底层原理和使用技巧。本文将从 数组的创建方式、内存模型、动态扩容机制,到 多种遍历方法及其性能差异,结合 ES6+ 的现代语法,深入剖析 JavaScript 中数组的方方面面。
🧱 数组的本质:连续内存 vs 离散结构
JavaScript 中的数组本质上是一种 特殊的对象,但它在大多数引擎(如 V8)中会被优化为 连续内存块(称为“快速元素”模式),前提是数组是密集的、类型一致的(如全是数字)。这种设计使得通过索引访问元素的时间复杂度为 O(1),效率极高。
const arr = [1, 2, 3, 4, 5, 6];
上述代码创建了一个包含 6 个整数的数组。在内存中:
arr变量本身存储在 栈内存 中,保存的是指向堆内存中数组对象的 引用地址。- 实际的
[1,2,3,4,5,6]数据存储在 堆内存 中,并以 连续空间 的形式排列。
✅ 连续内存的优势:CPU 缓存友好,访问速度快。
相比之下,链表 是一种离散结构,每个节点包含数据和指向下一个节点的指针。虽然插入/删除灵活,但访问任意元素需从头遍历,时间复杂度为 O(n)。因此,在元素数量不多、频繁随机访问的场景下,数组远优于链表。
🔨 数组的多种创建方式
1. 字面量方式(推荐)
const arr = [1, 2, 3, 4, 5, 6];
- 简洁、直观。
- 元素已知且数量固定时首选。
2. 构造函数创建空数组
const arr = new Array(); // 等价于 []
3. 指定长度但未初始化(⚠️ 注意!)
const arr2 = new Array(6);
console.log(arr2); // [empty × 6]
- 此时数组长度为 6,但 没有实际元素,是“稀疏数组”。
- 使用
for...in或forEach遍历时,这些“空槽”会被跳过!
4. 指定长度并填充默认值
const arr = (new Array(6)).fill(0);
console.log(arr); // [0, 0, 0, 0, 0, 0]
fill()方法将所有位置填充为指定值。- 非常适合初始化一个固定长度、初始值统一的数组(如 DP 表、计数器等)。
⚠️ 警告:
new Array(n)不等于[undefined, undefined, ..., undefined]!它是真正的“空洞”,不是undefined值。
🔄 数组的动态性与扩容代价
JavaScript 数组是 动态数组,支持在运行时自动扩容:
let arr = [1, 2, 3];
arr.push(4); // 自动扩容
但扩容并非免费:
- 当容量不足时,引擎会 申请一块更大的连续内存(通常是当前容量的 1.5~2 倍)。
- 将旧数据 复制 到新内存。
- 释放旧内存。
这个过程称为 “搬家”,开销较大。频繁扩容会导致性能下降。
💡 建议:若能预估数组大小,可预先分配足够空间,减少扩容次数。
const arr = new Array(1000).fill(0); // 预分配
相比之下,链表 插入无需移动数据,但牺牲了随机访问速度。因此,小规模、频繁访问 → 用数组;大规模、频繁增删 → 考虑链表或 Map/Set。
🏃♂️ 数组遍历方法全解析
JavaScript 提供了多种遍历数组的方式,各有优劣。
1. 🚀 计数循环(性能最佳)
const arr = new Array(6).fill(0);
const len = arr.length;
for (let i = 0; i < len; i++) {
console.log(arr[i]);
}
- 优点:直接操作索引,无函数调用开销,CPU 友好。
- 注意:缓存
arr.length避免每次读取属性(虽现代引擎已优化,但仍是好习惯)。
✅ 在性能敏感场景(如游戏循环、大数据处理)中,这是首选。
2. 🔁 for...of(ES6,可读性好)
const arr = [1, 2, 3, 4, 5, 6];
for (let item of arr) {
console.log(item);
}
- 遍历 值,而非索引。
- 支持
break、continue。 - 内部使用迭代器协议,兼容所有可迭代对象(如 Set、Map、String)。
✅ 推荐用于日常开发,简洁安全。
3. 📜 forEach(函数式风格,但有限制)
const arr = [1, 2, 3, 4, 5, 6];
arr.forEach((item, index) => {
if (item === 3) {
break; // ❌ 语法错误!不能 break
}
console.log(item, index);
});
- 缺点:
- 无法使用
break或continue(只能用return跳过当前迭代)。 - 每次调用回调函数,涉及 函数入栈/出栈,性能略低于 for 循环。
- 无法使用
- 优点:语义清晰,适合纯遍历操作。
⚠️ 若需中途退出,应改用
for...of或传统 for。
4. 🛠️ map(用于转换,返回新数组)
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.map(item => item + 1);
console.log(newArr); // [2, 3, 4, 5, 6, 7]
- 不修改原数组,返回一个新数组。
- 适用于“加工”场景:如格式化、计算、映射等。
- 性能开销大于
forEach(因需构建新数组)。
✅ 函数式编程核心方法之一。
5. 🔍 for...in(慎用!)
const obj = { name: "黄国文", age: 18 };
for (let k in obj) {
console.log(k, obj[k]); // 遍历对象属性
}
const arr = [1, 2, 3, 4, 5, 6];
for (let key in arr) {
console.log(key, arr[key]); // key 是字符串 "0", "1", ...
}
for...in遍历对象的 可枚举属性名(包括继承的!)。- 数组也是对象,其索引被视为字符串键。
- 问题:
- 遍历顺序不保证(尽管现代引擎通常按索引顺序)。
- 会遍历非数字属性(如
arr.customProp = 'x'也会被遍历)。 - key 是字符串,需转数字才能用于计算。
❌ 不推荐用于数组遍历!应使用
for...of或forEach。
⏳ 异步陷阱:闭包与变量作用域
来看一个经典面试题:
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
输出:0, 1, 2, ..., 9 ✅
为什么?因为 let 声明具有 块级作用域,每次循环都会创建一个新的词法环境,setTimeout 回调捕获的是各自循环中的 i。
如果换成 var:
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出 10 次 10!
}, 1000);
}
var是函数作用域,所有回调共享同一个i,循环结束后i === 10。
✅ 使用
let是解决此问题的现代方案。
🧩 总结:何时用哪种方式?
| 场景 | 推荐方法 |
|---|---|
| 高性能遍历(如渲染、计算) | for (let i = 0; i < len; i++) |
| 日常遍历,需中途退出 | for...of |
| 纯遍历,无中断需求 | forEach |
| 数组转换、映射 | map |
| 初始化固定长度数组 | new Array(n).fill(value) |
| 避免 | for...in 遍历数组 |
🌟 结语
数组虽小,乾坤很大。
理解其内存布局、动态机制与遍历特性,不仅能写出更高效的代码,还能避开诸多陷阱。
在现代 JavaScript 开发中,结合性能需求与代码可读性,选择最合适的工具,才是高手之道。
💬 记住:“数组是连续的,链表是指针的;遍历千万条,for 循环最可靠。”