从内存视角重新认识 JavaScript 数据类型:一份深度学习笔记

📚 从内存视角重新认识 JavaScript 数据类型:一份深度学习笔记

前端八股文背了一堆,但你真的理解 nullundefined 的本质区别吗?知道 0.1 + 0.2 背后的底层原理吗?本文从冯·诺依曼架构出发,带你用内存模型的视角重新审视 JavaScript 的 8 种数据类型。


🤔 写在前面

相信很多前端同学都有这样的经历:面试被问到"JavaScript 有几种数据类型",可以脱口而出"7种基本类型+1种引用类型"。但当面试官继续追问:

  • nullundefined 到底有什么本质区别?
  • 为什么 0.1 + 0.2 !== 0.3
  • Symbol 存在的意义是什么?

这时候就开始含糊其辞了。

最近在系统性地重新学习 JavaScript 基础,我决定不再停留在"是什么"的层面,而是从内存模型的角度去理解每一种数据类型。这篇文章就是我的学习笔记,希望对你也有帮助。


🏗️ 先聊聊内存:从冯·诺依曼说起

在深入数据类型之前,我们先建立一个底层认知框架。

JavaScript 代码最终是要在计算机上运行的,而现代计算机的基石就是冯·诺依曼架构------由运算器、控制器、存储器、输入设备和输出设备组成。代码存储在磁盘上,运行时被加载到内存中。

这里有两个关键概念需要理解:

栈内存(Stack)

  • 速度快,容量小
  • 存储基本类型的值(Number、String、Boolean、null、undefined、Symbol、BigInt)
  • 编译器会预先计算函数需要的内存空间
  • 函数执行完毕,直接移动栈指针即可释放内存,效率极高
  • 打个比方:就像叠盘子,放一个上去、取一个下来,后进先出

堆内存(Heap)

  • 速度相对较慢,容量大
  • 存储引用类型的值(Object、Array、Function 等)
  • 内存大小不固定,需要动态分配
  • 需要垃圾回收机制(GC)来管理生命周期

理解了这个基础,接下来的内容就会豁然开朗。


🔍 null vs undefined:两个"空"的前世今生

这两个是面试中的常客,也是最容易混淆的一对。让我们从代码出发:

null------"我主动说这里没有东西"

javascript 复制代码
// null 表示一个变量被显式地赋值为"空"
let user = null;
console.log(user); // null
console.log(typeof user); // "object" (这是 JS 的一个历史遗留 bug!)

// 常见使用场景:手动释放内存
let largeObject = new Array(10000000).fill('占用大量内存');
// ... 使用完毕后
largeObject = null; // 主动释放,帮助垃圾回收器回收内存

💡 划重点null 是开发者主动 赋值的,表示"这里应该有一个值,但目前没有"。当你想清空一个变量、释放内存的时候,就把它设为 null

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

undefined 是 JavaScript 引擎自动赋予的默认值,代表"这里还没有被赋值"。

本质区别

维度 null undefined
赋值方式 开发者主动赋值 引擎自动赋予
语义 "有这个变量,但值为空" "还没有被初始化"
typeof "object"(历史 bug) "undefined"
数值转换 Number(null) → 0 Number(undefined) → NaN

一句话总结:null 是程序员的主动选择,undefined 是引擎的默认行为。


💡 值传递 vs 引用传递:复印机的故事

理解了栈内存和堆内存之后,我们来看一个非常重要的概念------拷贝行为

基本类型:复印机模式

javascript 复制代码
let a = null;
let b = a; // 把 a 的值"复印"一份给 b
b = 2;

console.log(a); // null(a 不受影响)
console.log(b); // 2

基本类型存储在栈内存中,赋值时就像复印机------把值原样复制一份。修改副本不会影响原件。

引用类型:共享遥控器模式

javascript 复制代码
let obj = { name: '张三', company: '掘金' };
let obj2 = obj; // obj2 拿到的是同一个内存地址(遥控器)

obj2.company = '字节跳动';
console.log(obj.company); // "字节跳动"(obj 也被改了!)

引用类型存储在堆内存中,变量里存的不是值本身,而是一个内存地址 (可以理解为遥控器)。赋值时只是把地址复制了一份,两个变量指向同一块内存。所以修改 obj2 会影响 obj

⚠️ 这就是为什么在 Vue/React 中,我们经常需要做深拷贝来避免数据污染的底层原因!


🔢 Number 与 BigInt:0.1 + 0.2 的千古之谜

浮点数精度问题

javascript 复制代码
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false!

这个经典面试题的答案其实很简单:JavaScript 使用 IEEE 754 双精度浮点数 标准来表示数字。在这种标准下,很多十进制小数(比如 0.1)在二进制中是无限循环的:

复制代码
0.1 → 0.0001100110011001100110011...(二进制,无限循环)
0.2 → 0.001100110011001100110011...(二进制,无限循环)

计算机只能存储有限位数,截断后就会产生微小的误差。两个有误差的值相加,误差就显现出来了。

实际开发中的解决方案:

javascript 复制代码
// 方案一:使用 toFixed 控制精度
console.log((0.1 + 0.2).toFixed(1)); // "0.3"

// 方案二:转换为整数运算
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3

// 方案三:使用第三方库如 decimal.js

BigInt------大数救星

JavaScript 的 Number 类型能安全表示的最大整数是 2^53 - 1(即 Number.MAX_SAFE_INTEGER,约 9×10¹⁵)。超过这个范围就会丢失精度:

javascript 复制代码
console.log(9007199254740992 === 9007199254740993); // true!精度丢失了

ES2020 引入了 BigInt 来解决这个问题:

javascript 复制代码
// 在整数后面加 n 就变成了 BigInt
const big1 = 9007199254740993n;
const big2 = 123456789012345678901234567890n;
const sum = big1 + big2;

console.log(typeof big1); // "bigint"
console.log(sum); // 123456789012345678901243568883n

// ⚠️ BigInt 和 Number 不能混合运算!
// console.log(big1 + 1); // TypeError!
console.log(big1 + 1n); // 9007199254740994n(两个都是 BigInt 就可以)

💡 记住:涉及金额计算、大数ID等场景,一定要考虑精度问题。这是线上事故的高频来源!


🏷️ Symbol:世界上独一无二的标识符

Symbol 是 ES6 引入的第七种基本类型,也是最容易被忽略的一种。

核心特性:绝对唯一

javascript 复制代码
const s1 = Symbol('掘金');
const s2 = Symbol('掘金');

console.log(s1 === s2); // false!即使描述相同,也绝不相等
console.log(typeof s1); // "symbol"

每个 Symbol() 调用都会创建一个全局唯一的值,参数只是描述标签,用于调试,不影响唯一性。

最大用途:作为对象的属性键

javascript 复制代码
const SECRET_KEY = Symbol('密钥');

const obj = {
  name: '张三',
  age: 25,
  [SECRET_KEY]: '只有我知道的秘密'
};

console.log(obj.name); // "张三"
console.log(obj[SECRET_KEY]); // "只有我知道的秘密"

// Symbol 属性不会出现在普通遍历中
console.log(Object.keys(obj)); // ["name", "age"](没有 SECRET_KEY)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(密钥)]

Symbol 属性的三大优势:

  1. 不会被意外覆盖:即使别人定义了同名字符串属性,也不会冲突
  2. 不会被遍历到for...inObject.keys() 都不会获取到 Symbol 属性
  3. 实现"半私有" :虽然不是真正的私有(可以通过 getOwnPropertySymbols 获取),但能有效防止误操作

在实际开发中,Symbol 广泛用于:

  • React 的 React.FragmentReact.Provider 等内部标识
  • Redux 的 Action Type 定义(避免命名冲突)
  • 模块内部的"私有"方法

📊 完整数据类型全景图

让我们把 8 种数据类型梳理成一张完整的图:

javascript 复制代码
JavaScript 数据类型
├── 基本类型(Primitive)------ 存储在栈内存,按值访问
│   ├── Number      (数字,含整数和浮点数)
│   ├── String      (字符串)
│   ├── Boolean     (布尔值:true / false)
│   ├── null        (主动赋值的"空")
│   ├── undefined   (未初始化的默认值)
│   ├── Symbol      (ES6,唯一标识符)
│   └── BigInt      (ES2020,任意精度整数)
│
└── 引用类型(Reference)------ 存储在堆内存,按地址访问
    └── Object      (对象,包括 Array、Function、Date、RegExp 等)

一张表搞定判断方式

数据类型 typeof 返回值 判断方式 注意事项
Number "number" typeof x === 'number' NaN 也是 "number",用 Number.isNaN()
String "string" typeof x === 'string' ---
Boolean "boolean" typeof x === 'boolean' ---
null "object" x === null typeof 的历史 bug
undefined "undefined" typeof x === 'undefined' ---
Symbol "symbol" typeof x === 'symbol' ---
BigInt "bigint" typeof x === 'bigint' ---
Object "object" Array.isArray(x) / instanceof typeof null 也是 "object"

🎯 面试高频问题速查

结合本文内容,整理几个高频面试题的"满分回答":

Q1:null 和 undefined 的区别?

null 表示开发者主动将变量清空,语义是"有这个变量但当前值为空";undefined 表示变量声明了但未赋值,是引擎赋予的默认值。typeof null 返回 "object" 是 JS 语言的一个历史遗留 Bug,不代表 null 是对象。

Q2:为什么 0.1 + 0.2 !== 0.3?

因为 JavaScript 使用 IEEE 754 双精度浮点数标准,0.1 和 0.2 在二进制中都是无限循环小数,存储时会有精度截断,截断后的值相加会产生微小的误差。

Q3:Symbol 有什么用?

Symbol 创建全局唯一的标识符,主要用作对象属性键来避免命名冲突。Symbol 属性不会被 for...inObject.keys() 遍历到,可以实现"半私有"的属性。

Q4:值传递和引用传递的区别?

JavaScript 中所有基本类型都是值传递(复制值本身),引用类型是引用传递(复制内存地址)。这就是为什么修改引用类型的一个变量会影响另一个指向同一内存的变量。


🧭 总结与思考

回顾整篇文章,其实 JavaScript 数据类型的学习可以归纳为一条主线、两个视角

一条主线:从底层内存模型理解数据类型------栈内存存储基本类型(快、小、按值),堆内存存储引用类型(慢、大、按地址)。

两个视角

  1. 是什么------每种类型的定义、特性和使用场景
  2. 为什么------语言设计背后的考量(为什么需要 BigInt?为什么需要 Symbol?为什么浮点数不精确?)

当我们不仅知道"是什么",还能解释"为什么"的时候,才算真正掌握了这些知识。


📝 写在最后 :这篇文章是我在系统学习 JavaScript 基础过程中的笔记整理。如果你也在重新审视基础,建议不要只看文章,把文中的代码都跑一遍,用自己的手和眼睛去验证每一个结论。纸上得来终觉浅,绝知此事要躬行。

如果觉得有帮助,点个 👍 支持一下吧~


参考资料:

  • ECMA-262 规范
  • MDN Web Docs
  • IEEE 754 浮点数标准
相关推荐
IVEN_1 小时前
记一次诡异的前端白屏故障:Nginx Proxy Cache 内存缓存"幽灵"事件
前端·nginx
如果超人不会飞1 小时前
TinyRobot SuggestionPills紧凑的建议按钮组组件
前端·vue.js
如果超人不会飞1 小时前
TinyRobot Container构建优雅的AI对话容器
前端·vue.js
Waay1 小时前
K8s ETCD 详解|备份恢复+静态Pod原理+kubectl查询底层流程(面试必考)
面试·kubernetes·etcd
幸运小圣1 小时前
全面解析 Web 核心性能指标:LCP、INP、CLS 是什么、怎么用、怎么看
前端
如果超人不会飞1 小时前
TinyRobot SuggestionPopover智能建议弹出框组件
前端·vue.js
LiuJun2Son1 小时前
Angular 快速入门:从零搭建你的第一个应用
前端·javascript·angular.js
烬羽1 小时前
从零理解树与二叉树:用 JS 带你手撕遍历和递归
javascript·数据结构
程序员二叉2 小时前
【JVM】OOM详解+JVM参数+FullGC排查+CPU飙高+死锁+内存泄漏+命令大全
java·开发语言·jvm·面试