JavaScript 数组创建与遍历全解析:从内存模型到性能实践

58 阅读5分钟

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, ...]
  • 稀疏数组在 mapforEach 中会跳过空位,导致意外行为。

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 循环缓存 lengthconst len = arr.length
  • ✅ 小数据量优先用数组,避免过早优化到链表
  • ✅ 需要中断 → 用 forfor...of
  • ✅ 需要转换 → 用 map
  • ✅ 避免 for...in 遍历数组
  • ✅ 循环变量一律用 let

掌握这些细节,你不仅能写出正确代码,更能写出高性能、可维护、无坑的代码。数组虽小,学问不小!

欢迎点赞收藏,关注我,持续输出硬核前端知识