JS 到底有多少种数据类型?从ECMA规范到内存本质,一文彻底搞懂

别再死记硬背了------这篇文章带你从内存分配的底层视角,重新理解 JavaScript 的 8 种数据类型。

开篇:一个经典面试题的"标准答案"

"JavaScript 有多少种数据类型?"

如果你刷过面试题,大概率能脱口而出:8 种。但当我追问你"为什么是 8 种?""null 和 undefined 到底有什么区别?""Symbol 和 BigInt 解决了什么痛点?""原始类型和引用类型在内存里到底长什么样?"------你能从底层原理讲清楚吗?

这篇文章不只是告诉你"是什么",更会带你从 ECMA262 规范、冯·诺依曼体系结构、调用栈与堆内存 的视角,把 JS 数据类型这件事彻底吃透。

一、ECMA262 规范怎么说?

根据 ECMA262 规范,JavaScript 的数据类型分为两大类:

类别 类型 说明
原始类型(Primitive) Undefined, Null, Boolean, String, Symbol, Number, BigInt 7
对象类型(Object) Object 1 种

其中 NumberBigInt 在规范中统一归类为 Numeric (数值类型),所以有时也会看到"6 种原始类型"的说法------实际上是把 Numeric 视为一个类别,其下再分为 Number 和 BigInt。但面试和日常讨论中,通常说 7 种原始类型 + 1 种对象类型 = 8 种

在 ES6 之前,JavaScript 只有 6 种类型:Undefined、Null、Boolean、String、Number、Object。ES6 新增了 Symbol ,ES2020 又新增了 BigInt,这才凑齐了今天的 8 种。

二、最常见的两个"空值":null vs undefined

这是初学者最容易混淆的一组类型,但它们的设计意图完全不同。

null ------ "故意为空"

null 表示一个被有意设置为空的对象引用。它是"此处应该有一个对象,但目前没有"的语义:

javascript 复制代码
let obj = {
  name: "Alice",
  address: null  // 地址暂时不知道,但我知道这个字段应该存在
}

console.log(obj.address) // null  ------ "我明确告诉你,这里是空的"
console.log(obj.age)     // undefined ------ "压根没这个字段"

null 还有一个实用的用途------手动释放内存

javascript 复制代码
let largeObject = {
  data: new Array(1000000).fill("hgh")
}
// 不再需要这个大对象时,赋值为 null
largeObject = null  // 切断引用链,让垃圾回收器回收这块内存

undefined ------ "压根不存在"

undefined 表示变量未被初始化,或者你要找的东西根本不存在。它出现的典型场景有四个:

javascript 复制代码
// 场景一:声明变量但未赋值
let a
console.log(a) // undefined

// 场景二:访问对象不存在的属性
let obj = {}
console.log(obj.prototype) // undefined

// 场景三:函数没有返回值
function noReturn() {
  // 没有 return
}
console.log(noReturn()) // undefined

// 场景四:访问不存在的数组索引
let arr = [1, 2, 3]
console.log(arr[3]) // undefined

一句话总结:null 是人类主动设置的空,undefined 是 JS 引擎告诉你的"这东西不存在"

三、Symbol ------ 独一无二的标识符

Symbol 是 ES6 新增的原始类型,它的核心能力就两个字:唯一

javascript 复制代码
console.log(Symbol('张老板') === Symbol('张老板')) // false
console.log(typeof Symbol('张老板'))               // 'symbol'

哪怕你传了相同的描述字符串(也叫 tag/label),每次调用 Symbol() 创建的都是一个全新的、独一无二的值。

Symbol 最常见的用途是防止对象属性名冲突

javascript 复制代码
let obj = {
  [Symbol()]: 'value',  // 用 Symbol 做属性名
  prop: "2"
}
// 用 Symbol 作为 key 的属性,不会被常规遍历访问到

这在大型项目中非常有用------当你给一个第三方对象添加属性时,用 Symbol 做 key 可以保证绝对不会和已有属性冲突。

四、BigInt ------ JS 终于能算大数了

一个经典的 JS 笑话:

javascript 复制代码
console.log(0.1 + 0.2) // 0.30000000000000004

这不是 JS 的 bug,而是 IEEE 754 双精度浮点数标准的固有问题------二进制无法精确表示某些十进制小数,就像十进制无法精确表示 1/3 一样。

但更致命的问题是:JS 的 Number 类型能安全表示的最大整数只有 2^53 - 1

BigInt 就是来解决这个问题的:

javascript 复制代码
let num1 = 999999999999999999999999999999999999999999999999999999999999999n
let num2 = 123456789098765433467324577654789008733233456899003466788924243n

console.log(num1 + num2)     // 正确的大整数相加结果
console.log(num1 + 1n)       // BigInt 只能和 BigInt 运算
console.log(typeof num1)     // 'bigint'

注意事项:BigInt 和 Number 不能混用num1 + 1 会直接报错,必须写成 num1 + 1n。这是 JS 在类型安全上向前迈出的一小步。

五、原始类型 vs 引用类型------内存分配的本质

这部分的面试题层出不穷,但如果你从内存分配的角度去理解,它就永远不会忘。

冯·诺依曼架构下的 JS 运行时

现代计算设备的根基是冯·诺依曼架构(运算器、控制器、存储器、输入设备、输出设备)。JS 代码的执行过程是:

  1. 代码存储在硬盘(外存)
  2. 执行时,代码从硬盘调入内存(RAM)
  3. JS 引擎创建执行上下文(变量环境 + 词法环境),逐行执行代码
  4. 函数调用时,执行上下文被推入调用栈

栈内存 vs 堆内存

ini 复制代码
调用栈(Stack)                  堆内存(Heap)
┌─────────────────┐              ┌──────────────────┐
│ 原始类型变量     │              │  对象(Object)   │
│ ─────────────── │              │  { name: "谢总",  │
│ a = null        │              │    company: "快手" │
│ b = 2           │              │  }                │
│                 │              │                   │
│ obj1 ───────────│──── 引用 ───▶│   地址 0x0012FF   │
│ obj2 ───────────│──── 引用 ───▶│   地址 0x0012FF   │
└─────────────────┘              └──────────────────┘
  • 原始类型 :值直接写入栈内存 。赋值时是拷贝式赋值------复印机一样,把值复制一份给新变量。
  • 引用类型(Object) :对象本体存在堆内存 中,栈内存里只存一个指向堆内存地址的指针。赋值时复制的是指针,所以多个变量会指向同一个对象。

代码验证:

javascript 复制代码
// 原始类型:拷贝式赋值
let a = null
let b = a    // b 拿到了 a 的值(null),是两个独立变量
b = 2        // 改 b 不影响 a
console.log(a) // null

// 引用类型:引用式赋值
let obj1 = { name: "谢总" }
let obj2 = obj1       // obj2 拿到了 obj1 的引用地址,指向同一个对象
obj2.company = "快手"  // 改 obj2 指向的对象,obj1 也会看到变化
console.log(obj1, obj2) // { name: "谢总", company: "快手" } × 2

为什么这样设计?

栈内存的特点是快,但空间小。原始类型大小固定,编译阶段就能精确算出需要多少空间------函数执行完出栈时,只需要移动栈顶指针就可以释放整块空间,极快、极稳。

而对象是动态的,运行时才确定大小,可能随时增删属性------这种"不确定的、可变的"数据,只能扔到堆里管理,栈里存一个固定大小的指针就好。

六、一张全景速查表

类型 分类 含义 示例 typeof 结果
Number 原始 数值(IEEE 754) 42, 0.1 'number'
BigInt 原始 任意精度整数 999n 'bigint'
String 原始 字符串 'hello' 'string'
Boolean 原始 布尔值 true, false 'boolean'
Undefined 原始 未定义 undefined 'undefined'
Null 原始 空值 null 'object' (历史bug)
Symbol 原始 唯一标识 Symbol() 'symbol'
Object 引用 对象/数组/函数等 {}, [] 'object' / 'function'

一个小彩蛋:typeof null === 'object' 是 JS 诞生之初就留下的历史遗留 bug,因为 null 在底层被标记为了对象类型的空指针。这个 bug 永远不会被修复,因为会破坏太多现有代码。

七、写在最后

回到最初的问题------"JS 有多少种数据类型?"

如果你的答案是"8 种",恭喜你,面试官会点点头。

但如果你的答案是:

"7 种原始类型加 1 种对象类型,共 8 种。其中原始类型不可变、存栈,赋值靠拷贝;对象存堆,赋值靠引用。ES6 加了 Symbol 解决属性冲突问题,ES2020 加了 BigInt 弥补 Number 的大数精度短板。null 和 undefined 虽然都表示'空',但 null 是人为设置的空对象引用,undefined 是引擎层面的'未初始化'。"

那你不是在背书------你是真的懂了。


本文代码示例基于 ECMAScript 2020+ 标准,运行环境推荐 Node.js 14+ 或现代浏览器。

参考规范:ECMA-262 --- ECMAScript® 2025 Language Specification


相关推荐
努力努力再努力wz1 小时前
【内存管理与高并发内存池系列】从 mmap 到 malloc:文件映射、匿名映射与 glibc 内存分配机制详解
linux·c语言·数据结构·数据库·c++·qt·链表
前端Hardy1 小时前
GitHub 爆火!Three.js + React + ECharts 打造最强数据大屏
前端·javascript
八解毒剂2 小时前
数据结构-平衡二叉树——对二叉搜索树的优化
数据结构·c++·算法
数据知道2 小时前
视觉伪装(下):WebGL 渲染器与厂商特征的底层伪造与屏蔽
javascript·数据采集·webgl·指纹浏览器
东风破_2 小时前
JS 数据类型:从八种分类到栈与堆的内存真相
javascript
YIAN2 小时前
# 从入门到封装:一文搞懂 Fetch API 所有用法(新手友好)
前端·javascript
xiaofeichaichai3 小时前
Tree Shaking
前端·javascript
Darling噜啦啦3 小时前
JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心
前端·javascript·数据结构
万少3 小时前
一封邮件,让我重新打开了搁置半年的鸿蒙应用
前端·javascript·后端