JS 数据类型:从八种分类到栈与堆的内存真相

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. 原始类型:写在栈上的"值本身"

原始类型的变量,存储的是值本身。给另一个变量赋值时,行为像复印机------复印一份新的,原件和复印件互不影响。

js 复制代码
let a = null;
let b = a;  // 拷贝值
b = 2;
console.log(a, b);

// 输出:null 2

b 不影响 a,因为 b 拿到的是一份独立拷贝。

2.1 null vs undefined:最容易混淆的两个原始类型

它俩长得像,但语义完全不同。

null ------ 空值。表示"这里应该有值,但目前没有",是你主动设的。

js 复制代码
let obj = {
  name: '张三',
  address: null // 地址应该有值,目前没收集到
};
console.log(obj.age);     // undefined --- 压根没定义过这个属性
console.log(obj.address); // null --- 定义了,但设成了空

null 还能用来手动释放内存:

js 复制代码
let largeObject = {
  data: new Array(100000000).fill('haha')
};
largeObject = null; // 切断引用,让垃圾回收器回收

undefined ------ 未定义。表示"这里根本没放过值",是系统的默认状态。四种典型场景:

js 复制代码
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。超出这个范围,精度就丢了。更糟的是------小数也算不准。

js 复制代码
let a = 0.1;
let b = 0.2;
console.log(a + b);

// 输出:0.30000000000000004

不是 JS 算错了,是二进制浮点数没法精确表示十进制小数------就像十进制没法精确表示 1/3(0.33333...)。有小数精度的场景(比如金额计算),要么用整数(单位换成分),要么用第三方库。

BigInt 解决的是大整数精度问题------数字末尾加 n 就是 BigInt 字面量:

js 复制代码
let num1 = 999999999999999999999999999999999999999999999999999999999999999n;
let num2 = 123456789098765433467324577654789008733233456899003466788924243n;
console.log(num1 + num2, typeof num1);

// 输出:(正确的大数结果) bigint

注意:BigInt 只能和 BigInt 做运算,不能混用 Number。 1n + 1 会直接报 TypeError

3.2 Symbol:独一无二的标识符

当你需要一个绝对不会和别人重复的 key,用 Symbol。

js 复制代码
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. 引用类型与内存:栈和堆的分工

解释完原始类型,回到开头那个让人困惑的现象:

js 复制代码
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.company = '快手';
console.log(obj1, obj2);

// 输出:
// { name: '张三', company: '快手' } { name: '张三', company: '快手' }

为什么改 obj2 会把 obj1 也改了?答案在内存里。

4.1 冯诺依曼架构:代码跑起来之后

现代计算设备都遵循冯诺依曼架构 ,由五部分组成:运算器、控制器、存储器、输入设备、输出设备

你写的 .js 文件存在硬盘(外存)上。执行时:

  1. 编译:代码从硬盘调入内存
  2. JS 引擎创建执行上下文(变量环境 + 词法环境 + 可执行代码)
  3. 执行上下文被推入调用栈(栈内存)

调用栈就是 JS 代码执行的地方。为什么执行上下文放在栈里? 因为函数执行上下文占用的空间是算得出来的------编译阶段就能确定需要多少内存,刚好合适。当一个函数执行完出栈,引擎只需要做一次指针偏移量切换 就能跳到新的栈顶,继续执行下一个上下文。所以栈的特点是:快速、稳定、可扩展

但栈和堆,分工不同:

维度 栈(Stack) 堆(Heap)
用途 存原始类型值 + 引用地址 存对象本身
大小 小,固定 大,动态分配
速度 快(偏移量切换) 慢(需要寻址)
生命周期 函数执行完自动回收 由垃圾回收器管理

4.2 原始类型 vs 引用类型的赋值行为

ini 复制代码
原始类型赋值(拷贝式):
  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. 如果只记住三件事

  1. 8 种类型,7 原 1 引 --- Number、String、Boolean、null、undefined、Symbol、BigInt 是原始类型,存的是值本身;Object(含数组、函数)是引用类型,存的是地址。
  2. null 是你设的,undefined 是引擎给的 --- 不混用就能避开大半类型相关的 bug。
  3. 拷贝和引用,取决于栈上存的是值还是地址 --- 原始类型赋值是"复印机",引用类型赋值是"同一个房子的两把钥匙"。

下一篇讲类型判断:typeofinstanceofObject.prototype.toString.call()------为什么 typeof null === 'object'?以及如何准确判断数组、函数、和自定义对象类型。

相关推荐
YIAN1 小时前
# 从入门到封装:一文搞懂 Fetch API 所有用法(新手友好)
前端·javascript
xiaofeichaichai1 小时前
Tree Shaking
前端·javascript
Darling噜啦啦2 小时前
JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心
前端·javascript·数据结构
万少2 小时前
一封邮件,让我重新打开了搁置半年的鸿蒙应用
前端·javascript·后端
To_OC2 小时前
从一段定时器代码,重新捋清 JS 同步、异步与 Promise
前端·javascript·代码规范
拙慕JULY3 小时前
小程序返回 base64 文件报错
开发语言·javascript·小程序
数据知道3 小时前
字体与排版防线:ClientRects 与系统字体枚举的底层拦截与伪造
javascript·数据采集·指纹浏览器·风控·浏览器指纹
一壶纱3 小时前
一个用于 UniApp 项目的 Pinia 持久化插件
前端·javascript·vue.js
凌涘3 小时前
JS 八大基本类型:一场内存视角的冒险之旅
前端·javascript