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


相关推荐
zephyr058 分钟前
数据结构-并查集
数据结构
稚南城才子,乌衣巷风流42 分钟前
动态树(Dynamic Tree)数据结构详解
数据结构·算法
Revolution611 小时前
一段 JavaScript 代码执行时,到底发生了什么
前端·javascript
zander2581 小时前
114. 二叉树展开为链表
数据结构·链表·深度优先
小肝一下2 小时前
3. 单链表
c语言·数据结构·c++·算法·leetcode·链表·dijkstra
tachibana22 小时前
hot100 前 K 个高频元素(347)
java·数据结构·算法·leetcode
程序员爱钓鱼3 小时前
Go 开发环境安装(Windows、macOS、Linux)
后端·面试·go
触底反弹3 小时前
🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
人工智能·设计模式·面试
uzong10 小时前
一个好的面试官,应该让候选人感到被尊重
面试
To_OC10 小时前
LC 131 分割回文串:刚学回溯时,我连怎么切字符串都想不明白
javascript·算法·leetcode