JavaScript 数组的内存存储方式
了解 JS 数组在堆栈中的存储机制和 V8 引擎的优化策略
问题
JavaScript 中数组是如何在内存中存储的?
解答
基本存储机制
JavaScript 数组不是以连续内存区域存储,而是采用哈希映射的形式。数组作为引用类型,其数据存储在堆内存中,栈内存中只保存指向堆内存的地址引用。
堆栈的区别
栈(Stack):连续的内存区域,按后进先出的顺序存放。主要存储基本类型的值(Boolean、Null、Undefined、Number、BigInt、String、Symbol)以及指向堆中对象的地址引用。栈的特点是访问速度快,但空间有限,生命周期短。
堆(Heap):不连续的内存区域,数据可以任意存放。主要存储对象和数组等引用类型。堆的特点是空间大,访问灵活,但速度相对较慢。
原始类型的不可变性
原始类型的值是不可变的。当修改一个原始类型变量时,实际上是在内存中创建了一个新值:
// 例子1:字符串不可变
var str = "abc";
str[0] = "d";
console.log(str); // "abc"
// 例子2:重新赋值创建新值
var str2 = "abc";
str2 = "dbc";
console.log(str2); // "dbc"
这样设计的原因:
- 安全性:避免多线程环境下的数据竞争和意外修改
- 共享性:值相同的变量可以共享同一内存区域,节省内存
V8 引擎中的对象存储
V8 将对象的属性分为两类存储:
- Elements:存储数字索引属性(数组元素)
- Properties:存储字符串属性
字符串属性的三种存储方式
1. In-object(对象内属性)
对象创建时预留的属性空间,直接存储在对象本身上,访问速度最快。通过字面量创建的空对象默认分配 4 个对象内属性空间。
2. Fast properties(快属性)
当对象内空间用完后,通过 HiddenClass(隐藏类)和 DescriptorArrays 来实现快速访问:
var a = {};
a.x = "value1";
a.y = "value2";
// 具有相同结构的对象共享同一个隐藏类
var b = {};
b.x = "value3";
b.y = "value4";
// a 和 b 拥有相同的隐藏类
隐藏类通过树状结构生成,每添加一个属性就创建一个新的隐藏类节点。相同结构的对象共享隐藏类,配合 Inline Caches(ICs)机制可以快速定位属性位置。
3. Slow properties(慢属性/字典模式)
当频繁增删属性时,维护隐藏类的开销过大,V8 会切换到字典模式。此时 DescriptorArrays 置空,属性直接以哈希表形式存储在 properties 数组中。
触发字典模式的情况:
- 动态添加过多属性
- 删除属性(使用 delete)
- 删除非最后添加的属性(V8 6.0+)
数组的特殊存储
数组元素存储在 elements 数组中,按索引访问。V8 针对不同场景做了优化:
普通数组:连续存储,访问效率高
const arr = ['a', 'b', 'c'];
稀疏数组:当数组存在大量空位时,切换为字典模式以节省内存
const sparseArray = [];
sparseArray[9999] = 'foo'; // 切换为字典模式
带空洞的数组:缺失元素会通过原型链查找
关键点
- JavaScript 数组作为引用类型存储在堆内存中,栈内存只保存引用地址
- 原始类型的值不可变,修改时会创建新值而非改变原值
- V8 将对象属性分为 Elements(数字索引)和 Properties(字符串属性)两类存储
- 对象属性有三种存储模式:In-object(最快)、Fast properties(隐藏类)、Slow properties(字典模式)
- 稀疏数组会自动切换为字典模式以节省内存空间
目录