JavaScript 类型系统详解:栈与堆的内存奥秘

26 阅读4分钟

JavaScript 数据类型的分类

JavaScript 中的数据类型分为两大类:基本类型(原始类型)和引用类型(复杂类型)。两者之间有什么区别呢?哪些类型是基本类型,哪些又是引用类型呢?

1. 基本类型(原始类型)

基本类型的值直接存储在栈内存中,具有固定大小,访问速度快,基本类型有如下几种:

  1. String,即字符串类型

代码示例

let str = 'hello world'//这是一个string类型的变量
  1. number,数字(整数/浮点数)类型,最大安全值为2的53次方,超出计算会出错

代码示例

let num = 123 //number类型
console.log(2**53 + 1)//最大安全值2的53次方,计算会出错

3.Boolean类型,布尔类型只有true和false两个值

代码示例

let isStudent = true//布尔类型
  1. undefined,在 JavaScript 中, undefined 既表示未定义值也是类型,使用 typeof 操作符可以看到它的类型:
var a = typeof undefined 
console.log(a)// "undefined"(类型名称)
  1. null,在 JavaScript 中,null同样即表示值也算类型,我们使用typeof 操作符来看看:
var b = typeof undefined
console.log(b)//Object

我们发现输出结果为Object,这是 JavaScript 中一个著名的历史遗留问题,在 JavaScript 最初的实现中:对象在内存中以 0x00 开头,null 被表示为全零,typeof 检查前几位判断类型,因此 null 被误判为 object

  1. bigint,大整数,当数字超出2的53次方时,想要准确运算必须使用bigint类型

代码示例

let bigNumber = 9007199254740991n;
let b = 3n
console.log(bigNumber + b)//9007199254740994n,结果正确
  1. symbol,Symbol 是 ES6 引入的第七种基本类型,用于创建 唯一标识符 。
const sym2 = Symbol("id");
const sym3 = Symbol("id");
conseole.log(sym2 === sym3);//false

可以看到即使sym2和sym3同样赋值为Symbol("id"),他们还是不相等

2. 引用类型(复杂类型)

引用类型的值存储在堆内存中,栈中只存储指向堆的引用地址。

  1. Array,数组类型,是有序元素的集合,数组增删值时若改变其他元素下标则会增加时间复杂度O(n)

代码示例

let numbers = [1, 2, 3, 4, 5] //这是一个Array类型的变量
numbers.push(6) //末尾添加:O(1)
numbers.pop() //末尾删除:O(1)
numbers.unshift(0) //开头添加:O(n),需要移动所有元素
numbers.shift() //开头删除:O(n),需要移动所有元素
  1. Object,对象类型,是键值对的集合

代码示例:

let person = { name: 'Bob', age: 30 } //这是一个Object类型的变量
  1. Function,函数类型,是可执行的代码块

代码示例:

let greet = function() { console.log('Hello') } //这是一个Function类型的变量
greet() //调用函数,输出Hello
  1. Date,日期类型,一个特殊的类型,用于表示日期时间对象

代码示例:

let time = new Date() //这是一个Date类型的变量
console.log(time) //输出当前日期时间

两种数据类型在V8 引擎的执行过程

1. 创建调用栈

调用栈(Call Stack)用于管理函数调用的执行上下文。它是一个 LIFO(后进先出)的数据结构,因此当栈内存过大时,执行效率低,堆由于其先进先出特性,不会影响执行效率

2. 创建执行上下文

执行上下文(Execution Context)即上期讲到的AOGO,包含了函数执行所需的所有信息:

3. 执行代码(栈与堆的交互)

代码执行时,不同类型的值会被存储在不同的内存区域,先看一段代码:

let str = 'hello'       // 基本类型:直接存栈中
let a = 1             // 基本类型:直接存栈中
let person = {           // 引用类型:值存堆中,地址存栈中
    name: 'Bob',
    age: 30
}
let numbers = [1, 2, 3]   // 引用类型:数组存堆中

内存布局如下:

┌─────────────────────────────────────────────────────────────┐
│                        内存布局                              │
├─────────────────────────────────────────────────────────────┤
│  栈内存 (Stack)                      │  堆内存 (Heap)        │
│                                     │                       │
│  ┌─────────────────┐                │ ┌─────────────────┐   │
│  │ str: "hello"    │                │ │                 │   │
│  ├─────────────────┤                │ │                 │   │
│  │ a: 1            │                │ │                 │   │
│  ├─────────────────┤                │ │                 │   │
│  │ person: 0x001   │ ──────────────→│ │ {name:"Bob",    │   │
│  │                 │                │ │  age:30}        │   │
│  ├─────────────────┤                │ │                 │   │
│  │ numbers: 0x002  │ ──────────────→│ │ [1, 2, 3]       │   │
│  └─────────────────┘                │ │                 │   │
│                                     │ └─────────────────┘   │
└─────────────────────────────────────────────────────────────┘

这种设计的好处:

  1. 性能优化:栈操作速度快,基本类型直接存栈中
  2. 内存效率:栈大小可控,避免过大影响执行效率
  3. 避免爆栈:引用类型存堆中,栈只存地址,节省栈空间