JS数据类型从八种分类到栈与堆的内存真相详析
作者:东风破_
let a = null;
let b = a;
b = 2;
console.log(a, b);
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.company = '快手';
console.log(obj1, obj2);
// 输出:
// null 2
// { name: '张三', company: '快手' } { name: '张三', company: '快手' }
同样是赋值,b = 2 没有影响 a,但 obj2.company = '快手' 却把 obj1 也改了。这不是 bug——这是 JS 数据类型在内存层面的根本差异。
立意句:理解 JS 八种数据类型的分类标准和内存存储差异,能用正确的类型判断和赋值方式避免 null/undefined 混淆、浮点数精度丢失、以及引用赋值带来的副作用。
1. 全景:JS 到底有多少种类型?
根据 ECMA262 规范,JS 共有 8 种数据类型,分两大阵营。其中原始类型中有一个 numeric 子分类——把 Number 和 BigInt 归在一起,因为它们都是"数值"。
| 分类 | 子分类 | 类型 | 说明 |
|---|---|---|---|
| 原始类型 | — | String | 字符串 |
| 原始类型 | — | Boolean | 布尔值 |
| 原始类型 | — | null | 有意设置为空的对象引用 |
| 原始类型 | — | undefined | 未初始化或不存在的值 |
| 原始类型 | — | Symbol(ES6 新增) | 唯一标识符 |
| 原始类型 | numeric | Number | 整数和浮点数统一(64位双精度) |
| 原始类型 | numeric | BigInt(ES6 新增) | 任意精度大整数 |
| 引用类型 | — | Object | 对象、数组、函数等 |
原始类型共 6 种(ES6 前)→ ES6 新增 Symbol 和 BigInt 后变成 7 种原始 + 1 种引用 = 8 种。记住这个分类,是后续所有讨论的底盘。
2. 原始类型:写在栈上的"值本身"
原始类型的变量,存储的是值本身。给另一个变量赋值时,行为像复印机——复印一份新的,原件和复印件互不影响。
let a = null; let b = a; // 拷贝值 b = 2; console.log(a, b); // 输出:null 2
改 b 不影响 a,因为 b 拿到的是一份独立拷贝。
2.1 null vs undefined:最容易混淆的两个原始类型
它俩长得像,但语义完全不同。
null —— 空值。表示"这里应该有值,但目前没有",是你主动设的。
let obj = {
name: '张三',
address: null // 地址应该有值,目前没收集到
};
console.log(obj.age); // undefined — 压根没定义过这个属性
console.log(obj.address); // null — 定义了,但设成了空
null 还能用来手动释放内存:
let largeObject = {
data: new Array(100000000).fill('haha')
};
largeObject = null; // 切断引用,让垃圾回收器回收
undefined —— 未定义。表示"这里根本没放过值",是系统的默认状态。四种典型场景:
let a; // 1. 声明变量但未赋值
console.log(a); // undefined
let obj = {};
console.log(obj.name); // 2. 访问对象不存在的属性 → undefined
function noReturn() {}
console.log(noReturn());// 3. 函数没有返回值 → undefined
let arr = [1, 2, 3];
console.log(arr[5]); // 4. 访问不存在的数组索引 → undefined
简单区分:你用等号写 null,JS 引擎自己给你 undefined。
3. ES6 新增的两个原始类型
3.1 BigInt:JS 终于能算大数了
JS 的数字用 64 位二进制存储,整数安全范围是 -(2^53 - 1) 到 2^53 - 1。超出这个范围,精度就丢了。更糟的是——小数也算不准。
let a = 0.1; let b = 0.2; console.log(a + b); // 输出:0.30000000000000004
不是 JS 算错了,是二进制浮点数没法精确表示十进制小数——就像十进制没法精确表示 1/3(0.33333…)。有小数精度的场景(比如金额计算),要么用整数(单位换成分),要么用第三方库。
BigInt 解决的是大整数精度问题——数字末尾加 n 就是 BigInt 字面量:
let num1 = 999999999999999999999999999999999999999999999999999999999999999n; let num2 = 123456789098765433467324577654789008733233456899003466788924243n; console.log(num1 + num2, typeof num1); // 输出:(正确的大数结果) bigint
注意:BigInt 只能和 BigInt 做运算,不能混用 Number。
1n + 1会直接报TypeError。
3.2 Symbol:独一无二的标识符
当你需要一个绝对不会和别人重复的 key,用 Symbol。
console.log(Symbol('张三') === Symbol('张三'));
// 输出:false — 即使标签相同,每次调用 Symbol() 都返回一个全新的值
console.log(typeof Symbol('张三'));
// 输出:'symbol'
let obj = {
[Symbol()]: 'value',
prop: '2'
};
// Symbol 作为 key 的属性,普通遍历(for...in、Object.keys)拿不到它
Symbol() 就是一个唯一值生成器——你传的字符串只是标签(方便调试),不是值本身。两次 Symbol('张三') 就像两个人同名同姓,但他们是不同的人。
4. 引用类型与内存:栈和堆的分工
解释完原始类型,回到开头那个让人困惑的现象:
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.company = '快手';
console.log(obj1, obj2);
// 输出:
// { name: '张三', company: '快手' } { name: '张三', company: '快手' }
为什么改 obj2 会把 obj1 也改了?答案在内存里。
4.1 冯诺依曼架构:代码跑起来之后
现代计算设备都遵循冯诺依曼架构,由五部分组成:运算器、控制器、存储器、输入设备、输出设备。
你写的 .js 文件存在硬盘(外存)上。执行时:
- 编译:代码从硬盘调入内存
- JS 引擎创建执行上下文(变量环境 + 词法环境 + 可执行代码)
- 执行上下文被推入调用栈(栈内存)
调用栈就是 JS 代码执行的地方。为什么执行上下文放在栈里? 因为函数执行上下文占用的空间是算得出来的——编译阶段就能确定需要多少内存,刚好合适。当一个函数执行完出栈,引擎只需要做一次指针偏移量切换就能跳到新的栈顶,继续执行下一个上下文。所以栈的特点是:快速、稳定、可扩展。
但栈和堆,分工不同:
| 维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 用途 | 存原始类型值 + 引用地址 | 存对象本身 |
| 大小 | 小,固定 | 大,动态分配 |
| 速度 | 快(偏移量切换) | 慢(需要寻址) |
| 生命周期 | 函数执行完自动回收 | 由垃圾回收器管理 |
4.2 原始类型 vs 引用类型的赋值行为
原始类型赋值(拷贝式):
let a = null;
let b = a; // 在栈上复制了一份 null
b = 2; // 改的是 b 自己的那份,a 不受影响
引用类型赋值(引用式):
let obj1 = { name: '张三' };
// 栈上存的是地址 0x001,指向堆上的 { name: '张三' }
let obj2 = obj1;
// 栈上复制了同一个地址 0x001,两个变量指向堆上的同一个对象
obj2.company = '快手';
// 沿着 0x001 找到堆上的对象,修改它——obj1 也指向它,所以 obj1 也"变了"
原始类型在栈上直接写值,拷贝时就真的"复制了一份"。引用类型在栈上只存了一个地址牌,拷贝时复制的是地址牌——两个牌子指向堆上的同一个房子。从一个门进去改了装修,另一个门进去看到的一样变了。
5. 如果只记住三件事
- 8 种类型,7 原 1 引 — Number、String、Boolean、null、undefined、Symbol、BigInt 是原始类型,存的是值本身;Object(含数组、函数)是引用类型,存的是地址。
- null 是你设的,undefined 是引擎给的 — 不混用就能避开大半类型相关的 bug。
- 拷贝和引用,取决于栈上存的是值还是地址 — 原始类型赋值是"复印机",引用类型赋值是"同一个房子的两把钥匙"。
到此这篇关于JS数据类型从八种分类到栈与堆的内存真相的文章就介绍到这了,更多相关JS数据类型分类到栈与堆的内存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
