深入掌握 JavaScript 数组:从内存模型到遍历性能的全流程解析

60 阅读3分钟

深入理解 JavaScript 数组:从内存到遍历

数组是 JavaScript 中最常用的数据结构。频繁使用并不等于理解透彻。要写出高性能、可维护的代码,你需要知道数组在内存中如何布局扩容时发生了什么、以及不同遍历方式的真实成本

核心问题

  • 为什么数组能做到 O(1) 随机访问?
  • 动态扩容背后真实开销是什么?
  • []new Array(N)new Array(N).fill() 有何差别?
  • 各种遍历方式的性能与适用场景是什么?

1) 数组的本质:连续内存 + 高效寻址

数组元素在内存中是连续存放的。访问 arr[index] 时,CPU 可以直接通过基地址 + offset 计算出元素地址,因此读取为 O(1)

这种连续性带来三大好处:

  • 随机访问极快(无需遍历)
  • CPU 缓存友好(cache locality)
  • 引擎可做连续存储级别的优化

结论:当需要频繁读取任意位置元素时,数组是首选。


2) push 的代价:数组并非无限“伸缩”

JS 数组支持动态增长,但底层并非无代价:

当当前容量不足时,引擎通常会:

  1. 申请一块更大的连续内存;
  2. 将旧数组的数据复制到新空间;
  3. 释放旧空间(即“搬家”)。

代价体现为:

  • 内存分配成本上升
  • 数据复制(拷贝)开销增大
  • 可能因为内存碎片导致申请失败或更慢

建议:

  • 小规模数组无需担心;
  • 对于大数组且频繁扩容的场景,预分配容量或采用更合适的数据结构(如链表、分段数组)更稳妥。

3) 三种创建方式的区别与优化建议

const a = [1,2,3];               // 字面量(推荐)
const b = new Array(6);          // 空槽数组(Holey)
const c = new Array(6).fill(0);  // 稠密数组(Packed)
  • 字面量 [] :内容与 shape 明确,引擎优化最好,性能优先。
  • new Array(N)(空槽) :创建“空槽”——并非真正填充元素,容易产生 hole,影响引擎优化。
  • new Array(N).fill(v) :创建稠密数组,每项都有值,性能和字面量接近,适合一次性初始化大数组。

实践建议:优先使用字面量或 fill 初始化;避免无谓地使用 new Array(N) 生成空槽数组。


4) 遍历对比:哪个最快,哪个最稳?

常见遍历方式的性能大致排序(从快到慢):

for(计数循环) > for...of ≈ forEach > map > for...in

推荐与解析

  • for(计数)

    for (let i = 0; i < arr.length; i++) { /* ... */ }
    

    最快:无回调、索引直接访问、JIT 优化空间大。适合性能敏感场景(大量数据、动画、计算密集型任务)。

  • for...of

    for (const item of arr) { /* ... */ }
    

    可读性佳、支持 break/continue。性能略逊 for,但实用性高。

  • forEach

    arr.forEach(item => { /* ... */ });
    

    语义清晰。但无法中断(不能 break),每次迭代有回调开销。适合短小、逻辑清晰的遍历。

  • map
    用于生成新数组:const newArr = arr.map(x => x + 1);
    若只是遍历且不需要返回新数组,应避免使用 map

  • for...in
    不推荐用于数组:会遍历原型链、顺序不保证,且性能最差。仅用于对象属性遍历。


5) 简要知识表

主题要点
底层结构连续内存 ⇒ O(1) 随机访问
扩容机制空间不足时整体搬家(申请 + 复制)
推荐创建[]new Array(n).fill(v)
遍历优先级for > for...offorEach > map > for...in
forEach 缺点无法中断,有回调开销
new Array(n)产生空槽,影响优化(避免)

总结:用熟数组,更要“用对”数组

掌握数组的内存特性常见操作成本,能帮你在真实项目中避免性能陷阱:

  • 大量随机读 → 用数组;
  • 预期大量扩容 → 预分配或改用更合适的结构;
  • 追求性能遍历 → 优先 for,业务代码建议 for...of
  • 初始化大数组 → 使用 fill 或字面量