JavaScript 数组创建与遍历全解析:从内存模型到性能实践
数组是前端开发中最基础、最常用的数据结构,但你是否真正理解它的底层机制?本文结合内存模型、初始化方式、动态扩容原理及多种遍历方法的性能差异,带你系统掌握 JavaScript 数组的核心要点。
一、数组的本质:引用地址 vs 堆内存
// arr 在栈内存中存储的是引用地址
// [1,2,3] 实际数据存放在堆内存之中
const arr = [1, 2, 3, 4, 5, 6];
- 变量
arr存储在栈中,保存的是堆内存中数组对象的引用地址; - 实际元素
[1,2,3,...]存储在堆内存,占用连续空间(理想情况下); - 这种设计使得数组赋值、传参高效(只传递引用)。
二、数组的创建方式与陷阱
1. 字面量:已知内容,直接创建
// 数组的创建,申请了 6 个单位的连续空间
// 数组里面的值也确定了
const arr = [1, 2, 3, 4, 5, 6];
✅ 最推荐的方式:简洁、高效、语义清晰。
2. 构造函数:空数组 or 指定长度
// 构造函数,创建了一个新的空数组
const arr1 = [];
const arr2 = new Array(); // 等价于 []
// 创建一个长度确定,但每个元素未定义的数组
const arr3 = new Array(6);
// console.log(arr3); // empty × 6(稀疏数组!)
⚠️ 严重警告:
new Array(6)创建的是稀疏数组(empty slots),不是[undefined, undefined, ...];- 稀疏数组在
map、forEach中会跳过空位,导致意外行为。
3. 安全初始化:.fill() 是关键
// 创建一个长度确定,同时每一个元素的值也确定的
const arr = (new Array(6)).fill(0);
console.log(arr); // [0, 0, 0, 0, 0, 0]
✅ 推荐写法:避免稀疏数组,确保每个位置都有有效值。
三、动态扩容:便利背后的代价
要多了浪费,要少了怎么办?
-
JS 数组支持动态扩容,但本质是“搬家”:
- 当容量不足时,引擎会申请更大连续内存;
- 将旧数据整体拷贝到新空间;
- 释放旧内存。
-
二次申请内存空间开销较大,尤其在大数据量下;
-
对比链表:
- 链表插入/删除快(离散内存);
- 但小数据量时,数组更优(缓存友好 + 无额外指针开销);
- “链表节点的实例化多个指针”带来额外内存与 GC 压力。
结论:小规模、读多写少 → 用数组;高频增删、未知长度 → 考虑链表或 Map/Set。
四、遍历数组:6 种方式深度剖析
1. 计数循环(for)—— 性能最优
const arr = (new Array(6)).fill(0);
const len = arr.length; // 对象的属性,有开销的!
// 计数循环 CPU 工作很契合
// 遍历数组方法千千万,计数循环性能最好
for (let i = 0; i < len; i++) { // 缓存 length,避免重复访问
console.log(arr[i]); // O(1) 随机访问
}
✅ 优势:
- 直接索引访问,时间复杂度 O(1) ;
- 无函数调用开销;
- 支持
break/continue。
2. forEach —— 无法中断的函数式遍历
// forEach
const arr = [1, 2, 3, 4, 5, 6];
// 不能 break
// 函数入栈出栈,性能差
arr.forEach((item, index) => {
if (item === 3) {
// break; // ❌ 无效!只能用 return 跳过当前迭代
}
console.log(item, index);
});
❌ 缺陷:
- 无法真正中断循环;
- 每次迭代都涉及函数入栈出栈,性能低于 for 循环;
- 不适合需要提前退出的场景。
3. map —— 遍历 + 转换,返回新数组
// map
// forEach 遍历数组之外,再加工,返回新数组
const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.map(item => item + 1); // [2,3,4,5,6,7]
✅ 用途:不可变数据处理,生成新数组。
4. for...of —— ES6 的可读性之选
const arr = [1, 2, 3, 4, 5, 6];
// ES6 新增遍历数组的方法 for of
// 可读性好
// 计数循环可读性差:i=0, i<; i++ 死板
for (let item of arr) {
console.log(item);
}
✅ 优点:
- 语法简洁,语义清晰;
- 支持
break/continue; - 遍历的是值本身,非索引。
5. for...in —— 为对象设计,慎用于数组!
const obj = {
name: 'inx177',
age: 18,
hobbies: ["唱", "跳", "Rap", "篮球"]
};
// 设计来迭代对象的属性的
for (let k in obj) {
console.log(k, obj[k]);
}
// 数组也是对象,把数组看待成下标为 key 的可迭代对象
const arr = [1, 2, 3, 4, 5, 6];
// key 是下标(字符串!)
for (let key in arr) {
console.log(key, arr[key]); // key: "0", "1", ...
}
❌ 风险:
key是字符串类型("0", "1"…),非数字;- 会遍历原型链上的可枚举属性(若未过滤);
- 不推荐用于数组遍历。
6. 作用域陷阱:var vs let 的经典问题
// for (var i = 0; i < 10; i++) {
// setTimeout(function(){
// // 函数作用域
// console.log(i); // 10个10
// },1000)
// console.log(i); // 0 1 2 3 4 5 6 7 8 9
// }
for (let i = 0; i < 10; i++) {
// i 属于他自己的词法环境
// 块级作用域
setTimeout(function(){
// 函数作用域
console.log(i); // 0,1,2,...,9
}, 1000);
console.log(i); // 0 1 2 ... 9
}
关键点:
var是函数作用域,闭包共享同一个i;let创建块级作用域,每次循环都有独立的i;- 永远用
let写 for 循环!
五、数据结构全景:数组只是起点
- 数据结构要搞哪些?
## 线性数据结构
- 数组:常用、好用、连续内存
- 栈:先进后出(FILO)
- 队列:先进先出(FIFO)
- 链表:离散的
## 非线性数据结构
- 树(二叉树)
数组是开箱即用的数据结构,但并非万能。理解其适用边界,才能在复杂场景中做出合理选择。
六、总结:最佳实践清单
- ✅ 初始化定长数组:
new Array(n).fill(value) - ✅ for 循环缓存
length:const len = arr.length - ✅ 小数据量优先用数组,避免过早优化到链表
- ✅ 需要中断 → 用
for或for...of - ✅ 需要转换 → 用
map - ✅ 避免
for...in遍历数组 - ✅ 循环变量一律用
let
掌握这些细节,你不仅能写出正确代码,更能写出高性能、可维护、无坑的代码。数组虽小,学问不小!
欢迎点赞收藏,关注我,持续输出硬核前端知识