JS 数据类型的八重人格与内存真相

0.1+0.2 !== 0.3null 不是对象,那些面试官爱问但你似懂非懂的底层细节


开场:一个被面试题暴击的下午

想象一下,你坐在面试官对面,自信满满。面试官问:"JavaScript 有几种数据类型?"你脱口而出:"六种!"面试官微微一笑:"那是 ES6 以前。"接着他问:"0.1 + 0.2 等于多少?"你心想这不是侮辱我吗,脱口而出:"0.3。"面试官又笑了。那一刻你意识到,JS 的基础类型看似平平无奇,实则处处是坑。今天我们就把这八种类型的老底掀开,看看它们到底在内存里捣什么鬼。


第一幕:八张身份证------JS 到底认识几种类型?

ECMA-262 规范就像 JS 的户口本,上面白纸黑字写着:八种 。ES6 以前只有六种,后来新增了两个"刺头"------SymbolBigInt。具体名单如下:

**原始类型(Primitive Types)**就像是复印机印出来的独立文件:

  • Number:数值
  • Boolean:布尔值
  • String:字符串
  • null:空引用
  • undefined:未初始化
  • Symbol:唯一标识(ES6 新增)
  • BigInt:大整数(ES6 新增)

**复杂类型(Complex Type)**就像是共享文档:

  • Object:对象、数组、函数都归它管

但这里埋着 JS 历史上最著名的 Bug:

js 复制代码
console.log(typeof null); // "object"

这不是设计,这是遗产。当年 JS 引擎把 null 的标记位做成了和对象一样的 000,结果一传就是二十多年,修不了了。知道有八种只是开始,可怕的是它们住在内存的不同楼层------有的住快捷酒店(栈),有的住大别墅(堆)。


第二幕:复印机 vs 共享文档------原始类型与引用类型的内存分家

你复印了一份文件,在复印件上涂鸦,原件毫发无伤。但如果你打开一个共享文档改一个字,所有拿到链接的人,屏幕上的内容都会同步变。这就是原始类型和引用类型最核心的区别。

看看下面这段代码:

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

ab 各自在栈内存里有一块独立的小格子。b = 2 只是把 b 格子里原来的 null 擦掉,写上 2a 那边完全不受影响。这就是拷贝式赋值------像复印机,各玩各的。

但对象就完全是另一回事了:

js 复制代码
let obj1 = { name: "谢鲁立" };
let obj2 = obj1;
obj2.company = "快手";
console.log(obj1); // { name: "谢鲁立", company: "快手" }

为什么改了 obj2obj1 也跟着变了?因为它们在栈里存的根本不是对象本身,而是对象在堆内存里的地址

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        栈 内 存(Stack)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   变量 a    │   null                                        │
│   变量 b    │   2          ← 修改 b 只是给 b 换了张新便签     │
│                                                             │
│   变量 obj1 │   0x1001 ───────────────────┐                 │
│   变量 obj2 │   0x1001 ───────────────────┤                 │
│                                           │                 │
└───────────────────────────────────────────┼─────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────┐
│                        堆 内 存(Heap)                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   地址 0x1001  │  { name: "谢鲁立", company: "快手" }        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

函数执行时,JS 引擎会把执行上下文推入调用栈。栈这玩意儿的特点是快、稳定、空间小,而且函数执行完直接出栈,指针偏移一下就能切换上下文,非常适合存固定大小的原始类型和对象地址。

ini 复制代码
   代码开始执行
        │
        ▼
┌─────────────────┐
│  全局执行上下文  │ ──→ 压入调用栈
│  (Global EC)    │
└─────────────────┘
        │
        ▼
┌─────────────────┐
│ 调用栈(Stack)  │
├─────────────────┤
│  全局上下文     │ ◀── 栈底
│  ─────────────  │
│  变量环境       │ a = null
│  词法环境       │ obj1 = 0x1001
└─────────────────┘
        │
        ▼
   函数执行完毕
        │
        ▼
   出栈(Pop)──→ 指针偏移,上下文快速切换

所以当你写下面这行代码时,不要觉得自己在强迫症发作:

js 复制代码
let largeObject = {
    data: new Array(100000000).fill("hgh")
};
largeObject = null; // 手动断开引用,告诉垃圾回收器:这仓库我不要了

你这是在手动给内存松绑。


第三幕:null 是对象?undefined 是未定义?------一对卧底的真假身份

前面说了 typeof null === 'object' 是个官方 Bug,那 nullundefined 到底有啥区别?一句话:null 是"我故意把这里腾空",undefined 是"这东西还没出生"。

undefined 在代码里有四种经典出场方式:

js 复制代码
let a;
console.log(a); // undefined ------ 声明了变量,但没赋值

let obj = {};
console.log(obj.property); // undefined ------ 访问不存在的属性

function noReturn() {}
noReturn(); // undefined ------ 函数没有 return

let arr = [1, 2, 3];
console.log(arr[5]); // undefined ------ 访问越界的数组索引

换句话说,undefined 是 JS 引擎发给你的"空白支票"------表示"此处应该有值,但我还没想好放什么"。而 null 是你自己签的"腾空确认书"------表示"这里原来有东西,现在被我清空了"。

javascript 复制代码
    变量声明
       │
       ▼
┌──────────────┐
│   undefined  │  ← "这东西存在,但我还没想好放什么"
└──────────────┘
       │
       ▼
   显式赋值
       │
   ┌───┴───┐
   ▼       ▼
┌──────┐ ┌──────┐
│ 值   │ │ null │  ← "我故意把这里腾空,为了释放内存"
└──────┘ └──────┘

面试官最爱问的送命题来了:null == undefinedtrue,但 null === undefinedfalse。因为双等号会做类型转换,而三等号不会。这哥俩就像一对双胞胎,长得像,但户口本不一样。


第四幕:当数字背叛了你------0.1+0.2BigInt 的救世

小学数学老师告诉我 0.1 + 0.2 = 0.3,但 JS 给我的答案是:

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

为什么?因为 JS 统一使用 IEEE 754 双精度浮点数来存储所有数字。0.1 转成二进制是 0.0001100110011...,一个无限循环小数。64 位的存储空间装不下无限循环,只能截断,截断就丢了精度,丢了精度再转回十进制,就给你整出了这么个小尾巴。

markdown 复制代码
   十进制 0.1
       │
       ▼
   二进制转换
       │
       ▼
┌──────────────────────┐
│ 0.0001100110011...   │  ← 无限循环,存不下
│ (无限循环小数)      │
└──────────────────────┘
       │
       ▼
   截断存储(64 位)
       │
       ▼
   精度丢失
       │
       ▼
   0.1 + 0.2 ≠ 0.3

如果你要算钱,千万别用原生 Number 直接加减,要么转成整数算,要么用专门的库。但要是你非要处理一个连科学计数法都装不下的巨数,ES6 给你准备了 BigInt

js 复制代码
let num1 = 999999999999999999999999999999999999999999999999999999999999999n;
let num2 = 123456789098765433467324577654789008733233456899003466788924243n;
console.log(num1 + num2, typeof num1); // "bigint"
console.log(num1 + 1n); // BigInt 只能和 BigInt 运算

注意末尾那个 n,它就是 BigInt 的身份证。BigIntNumber 不能混算,强行混用会直接报错。BigInt 的出现不是为了替代 Number,而是为了填补那个"大到连 Infinity 都嫌小"的空白地带。


第五幕:Symbol------给属性上把无法复制的锁

假设你写了一个工具库,想偷偷给用户的对象塞一个内部标记,但又怕用户不小心用同名属性把你覆盖了。怎么办?Symbol 就是来解决这种" naming conflict "焦虑的。

js 复制代码
console.log(Symbol('张志恒') === Symbol('张志恒')); // false
console.log(typeof Symbol('张志恒')); // "symbol"

即使标签一模一样,每次调用 Symbol() 都会生成一个绝对唯一的标识符。

js 复制代码
let obj = {
    [Symbol()]: 'value',
    prop: "2"
};
javascript 复制代码
   Symbol('A')
       │
       ▼
   ┌────────┐    ┌────────┐
   │ UUID 1 │ ≠  │ UUID 2 │    ← 即使标签相同,内核也不同
   └────────┘    └────────┘
       │              │
       ▼              ▼
   Symbol('A')    Symbol('A')

更妙的是,Symbol 属性默认不会被 for...inObject.keys()JSON.stringify() 遍历到。你可以把它当作"半私有属性"来用------不是真私有,但足够低调,不轻易被外界打扰。如果你需要全局共享同一个 Symbol,可以用 Symbol.for('key') 在全局注册表里查找或创建,但大部分场景下,匿名 Symbol() 就是你最安全的命名空间隔离器。


结尾:类型是语法,内存才是真相

很多教程只教你怎么用 typeof,却不告诉你变量在内存里到底长什么样。但懂了栈和堆,你就突然明白为什么数据会"意外变化";懂了 nullundefined 的分工,你就不会在清空变量时手抖;懂了 0.1 + 0.2 的委屈,你就不会再被面试官的微笑吓到。

JS 的八种类型,表面上是一套语法规则,骨子里是一场关于内存地址的博弈。下次有人再问你 JS 有几种类型,你可以淡定地说------八种,外加一个永远的 Bug

相关推荐
星辰徐哥2 小时前
工具推荐:HTML5+AI开发必备的前端调试工具
前端·人工智能·html5
Full Stack Developme2 小时前
Linux Shell 教程概览
linux·前端·chrome
Maimai108082 小时前
Web3 前端实时通信如何落地:从 SSE 订阅到行情、订单与账户状态更新
前端·javascript·react.js·前端框架·web3·状态模式
星辰徐哥2 小时前
技能提升:自然语言处理在HTML5前端的应用
前端·自然语言处理·html5
the_answer2 小时前
React Server Components 深度剖析:前端架构的范式革命
前端
徐小夕2 小时前
我们放弃了单Agent方案:HiCAD 3.0 用 Harness 做多Agent编排,把3D建模的准确率提升了30%
前端·算法·github
胡萝卜术2 小时前
从零搞懂 AJAX:手把手带你从 XMLHttpRequest 到 fetch,彻底理解前后端数据交互
前端·后端·面试
星河耀银海2 小时前
接口调用:HTML5前端调用AI接口的基础语法与示例
前端·人工智能·html5
阿黎梨梨2 小时前
二分查找进阶:在排序数组中寻找元素的边界
javascript