JavaScript变量内存结构解析

97 阅读4分钟

理解JavaScript的内存结构,关键在于理解栈(Stack)堆(Heap)的关系。JavaScript引擎通过这种方式来管理内存。

核心概念:栈内存和堆内存

JavaScript引擎将内存分为两大区域:
1.栈内存

  • 特点: 有序、大小固定、操作简单高效(LIFO:后进先出)。系统自动分配和释放。
  • 存储内容:存储原始类型的值和指向堆内存中引用类型的指针(地址)。
  • 示例:Number、String、Boolean、Null、Undefined、Symbol、BigInt、对象引用(堆内存中的内存地址),这些类型的值直接存储在变量所在的栈内存中。

2.堆内存

  • 特点:无序、大小动态分配、结构更复杂。内存的分配和释放由JavaScript的垃圾回收机制管理。
  • 存储内容:储存引用类型的值,如:Object、Array、Function、Date等。
  • 示例:当你创建一个对象或者数组时,这个对象本身存储在堆内存中。

变量的内存分配过程

1.原始类型的存储
栈内存中会存储两种变量数据:原始数据和对象引用(堆内存中的内存地址)

  let a = 10;
  let b = ‘hello‘;
  let c = true;

内存结构图:

image.png

  • 过程:声明变量 a, b, c 后,JavaScript 引擎在栈内存中直接为它们开辟空间,并将原始值直接存储进去。

2.引用类型的存储

  let obj1 = { name: ‘Alice‘ };
  let arr1 = [1, 2, 3];

内存结构图:

image2.png

  • 过程:
    • 声明变量 obj1。
    • 在堆内存中创建一个新对象 { name: ‘Alice‘ },并分配一个内存地址(例如 0x001)。
    • 在栈内存中为变量 obj1 开辟一个空间,但这个空间存储的不是对象本身,而是对象在堆内存中的地址(0x001),变量obj1就可以通过引用地址0x001找到对内存中引用地址是0x001的对象。
    • 数组 arr1 的创建过程完全相同。
  • 这就是为什么它们叫“引用类型”,因为变量名存储的是对堆内存中实际值的“引用”(即地址)。

赋值

由于不同的存储机制,原始类型和引用类型在赋值行为完全不同。
1.原始类型赋值:值的复制

  • 声明一个原始类型变量时,JavaScript引擎会在栈内存中开辟一块内存来存储变量的值。
  • 修改原始类型变量的值时,会重新开辟一块新的内存存来储新的值,并将变量指向新的内存空间,而不是改变原来那块内存里的值。
  • 将一个原始类型变量赋值给另一个的变量时,会重新开辟一块新的内存,并将源变量内存里的值复制一份到新的内存里。
  let a = 10;
  // 将 a 的值 ‘10‘ 复制一份,赋值给 b
  let b = a; 

  // 修改 a 的值
  a = 20;    

  console.log(a); // 20
  console.log(b); // 10 (b 的值不受 a 的影响)

内存结构图:
image3.png 栈内存中的原始值一旦确定就不能被更改(不可变的),重新赋值就会重新开辟新的内存来存储。
2.引用类型赋值:引用的复制

  • 声明一个引用类型的变量时,JavaScript引擎会先在堆内存中开辟一块内存来储存对象,并在栈内存中开辟一块新的内存来储存对象的引用(堆内存地址),最后将变量指向这块栈内存。
  • 把引用类型变量赋值给另一个变量时,会将对象在栈内存中的引用地址复制给新变量存储到栈内存中,所以只是复制了个对象引用,并没有在堆内存中生成一份新的对象。
  • 而给引用类型变量分配为一个新的对象时,则会直接修改变量指向的栈内存中的引用,新的引用指向堆内存中新的对象。
  let obj1 = { name: ‘Alice‘ };
  // 将 obj1 的地址 ‘0x001‘ 复制一份,赋值给 obj2
  let obj2 = obj1; 
  // 通过地址 ‘0x001‘ 找到堆内存中的对象并修改它
  obj1.name = ‘Bob‘; 

  console.log(obj1.name); // ‘Bob‘
  console.log(obj2.name); // ‘Bob‘ (因为它们指向同一个对象)

  obj1 = {
    name: 'Yung'
  }
  console.log(obj1.name); // ‘Yung‘
  obj1.name = 'Selin'
  console.log(obj1.name); // ‘Selin‘
  console.log(obj2.name); // ‘Bob‘ (因为obj1指向一个新的对象)

内存结构图:

image5.png obj2 得到的是 obj1 存储的地址的副本,而不是对象的副本。现在两个变量都“指向”或“引用”堆内存中的同一个对象。

image6.png obj1 分配了一个新对象,会修改栈内存中的引用,新的引用指向堆内存中新的对象,栈内存中的对象引用是可以被更改的(可变的)。