JavaScript 8种数据类型详解:基本类型与引用类型的区别

作者:半夏

时间:2026-11-05

浏览:414

深入解析ECMAScript规范定义的8种JavaScript数据类型:7种基本数据类型与1种引用数据类型,详述栈内存与堆内存分配、传值与传址机制、类型判断决策与常见工程陷阱。

文章目录
分类:ECMAScript 规范核心 阅读建议:配合控制台与内存模型理解
在现行 ECMAScript 规范中,JavaScript 共有 8 种内置数据类型:7 种基本数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt)与 1 种引用数据类型(Object)。两者的本质分野在于数据在计算机内存中的存放形式与变量赋值时的传递机制——基本类型直接将值封存在栈内存中且本身不可修改,而引用类型将实体保存在堆内存中、变量仅持有栈中的内存指针。本文将逐一拆解 8 种类型特性,并从底层内存模型厘清两者的核心差异。
JavaScript 8种数据类型详解:基本类型与引用类型的区别
图 1:JavaScript 8 种数据类型及其两大类别归属全景图

一、7 种基本数据类型(Primitive Types)深度剖析

基本数据类型也常被称为“原始值”。在 JavaScript 中,原始值代表最底层、不可再细分的数据单位。它们具备一个根本特征:不可变性(Immutability)。无论调用多少方法,原始值本身永远不能被就地修改,所有看似修改的操作实际上都是在内存中创建并返回了一个崭新的值。

1. Undefined

Undefined 类型仅有一个合法值,即 undefined。当一个变量使用 let 或 var 声明但未赋予初始值时,运行时系统会自动将其缺省初始化为 undefined。此外,访问对象不存在的属性、函数没有显式返回值,或者未传入实参的形式参数,其求值结果均为 undefined。

let initialized;
console.log(initialized); // 输出: undefined
console.log(typeof initialized); // 输出: "undefined"

function noReturn() {}
console.log(noReturn()); // 输出: undefined

2. Null

Null 类型同样只有一个值,即 null。它在语义上表示“空对象指针”或“刻意缺失的对象引用”。在工程实践中,如果希望某个变量将来用于保存对象,通常在初始化时赋为 null;当一个对象不再需要使用并希望解除引用以配合垃圾回收时,也可以将其手动置为 null。

⚠️ 历史著名陷阱:typeof null === 'object'

执行 typeof null 时输出的是 "object"。这是 JavaScript 1995 年第一版实现遗留至今的 Bug。在最初的底层引擎中,值的机器码由 1~3 位的类型标签(Type Tag)与实际数据组成。对象的标签值是 000,而 null 的空指针表示为全零(0x00),导致判定标签时被误判为对象。由于大量已有网站依赖这一行为,该历史缺陷被永久保留在规范中。

3. Boolean

Boolean 仅包含两个逻辑值:true 和 false。在条件判断中,JavaScript 会自动将其他类型转换为布尔值。读者必须牢记以下 8 个被视作“假值(Falsy)”的特殊值,其余所有值(包括空对象 {} 与空数组 [])在转换为布尔值时均为真值(Truthy):

  • false
  • 0、-0、0n(BigInt 零)
  • ""(空字符串)
  • null
  • undefined
  • NaN

4. Number

JavaScript 中的 Number 采用 IEEE 754 标准的双精度 64 位二进制浮点数格式存储。它能够同时表示整数和浮点数,但这也带来了两个至关重要的技术约束:

  • 浮点精度舍入误差:二进制无法精确表示部分十进制小数(例如十进制的 0.1 和 0.2 在二进制中是无限循环小数),因此 0.1 + 0.2 === 0.3 的结果为 false(实际为 0.30000000000000004)。在金融等高敏感场景需使用整数(以分计数)或专用高精度库。
  • 安全整数区间:由于尾数位(Fraction)占 52 位加上 1 位隐含位,能够精确表示的整数范围为 [-2^53 + 1, 2^53 - 1],即 Number.MIN_SAFE_INTEGER 至 Number.MAX_SAFE_INTEGER(-9007199254740991 ~ 9007199254740991)。超出此区间的整数计算将丢失最低有效位精度。
  • 特殊数值:包含 Infinity(正无穷)、-Infinity(负无穷)以及 NaN(Not a Number,非数)。注意 NaN === NaN 的结果为 false,判断必须依赖 Number.isNaN(val)。

5. String

String 表示不可变的 UTF-16 代码单元序列。所有的字符串操作方法(如 slice、replace、toUpperCase)都不会修改原字符串,而是返回一个全新的字符串对象副本。

let str = "hello";
str[0] = "H"; // 严格模式下抛出 TypeError,静默失败模式下无效果
console.log(str); // 输出依然是 "hello"

let upper = str.toUpperCase(); // 返回全新字符串
console.log(str);   // "hello"(原值未动)
console.log(upper); // "HELLO"

6. Symbol(ES6 新增)

Symbol 是 ES6(ECMAScript 2015)引入的一种全新原始类型,用于生成全局唯一且不可变的值。每个由 Symbol() 函数创建的 symbol 实例都是独一无二的,即使传入相同的描述字符串也绝不相等。其核心应用场景是作为对象属性的键名,防止多人协作或第三方库扩展时的属性命名冲突,亦可作为实现私有属性的轻量手段。

const idA = Symbol("id");
const idB = Symbol("id");
console.log(idA === idB); // 输出: false

const user = {
  name: "Bob",
  [idA]: 1001 // 唯一键,不会被常规的 Object.keys() 遍历出
};

7. BigInt(ES2020 新增)

为了打破 Number 类型安全整数 2^53 - 1 的物理上限,ES2020 引入了 BigInt。它可以表示任意精度的整数。定义 BigInt 可以在整数字面量后附加字符 n,或调用 BigInt() 构造函数。

const maxSafe = Number.MAX_SAFE_INTEGER; // 9007199254740991
console.log(maxSafe + 1 === maxSafe + 2); // 输出 true,精度丢失!

const big1 = 9007199254740991n;
console.log(big1 + 1n === big1 + 2n); // 输出 false,精确计算!

// 注意:BigInt 不能与 Number 发生隐式混合运算,必须显式转换
// big1 + 10; // 抛出 TypeError: Cannot mix BigInt and other types

二、1 种引用数据类型(Reference Type)

引用数据类型统称为 Object。它是各种无序键值对及复杂数据结构的母体。在 JavaScript 中,“一切复杂结构皆对象”。除了普通的对象字面量({ key: value }),常见的数组(Array)、函数(Function)、日期(Date)、正则表达式(RegExp)、集合(Map / Set)在语言类型系统层面全属于 Object 的派生形态。

引用数据类型的核心特征是可变性(Mutability)。你可以在不改变对象本身物理引用的前提下,随时向其中添加、修改或删除属性。当多个变量指向同一内存实体时,通过任何一个变量对内部状态进行更改,其余变量读取到的内容都将同步改变。

三、基本类型与引用类型的 4 大核心区别

理解基本类型与引用类型的边界,关键在于跳出语法表象,从计算机内存与执行机制的角度观察。两者的差异主要体现在以下四个核心维度:

JavaScript 8种数据类型详解:基本类型与引用类型的区别
图 2:基本类型与引用类型在栈内存与堆内存中的物理存储架构

1. 内存分配与存储位置

  • 基本类型:存储在栈内存(Stack)中。栈内存是操作系统直接管理的连续内存空间,结构简单、读写速度极高,变量名与其具体数值直接绑定在同一个栈帧内存单元中。
  • 引用类型:实际数据存储在堆内存(Heap)中。堆内存是动态分配的不规则存储空间,支持动态扩容与弹性结构。在栈内存中,仅保留该对象在堆中的16进制内存地址指针。

2. 变量赋值机制(按值传递 vs 传指针副本)

当把一个变量赋值给另一个变量时,两者的表现完全迥异:

  • 基本类型按值复制:系统在栈中为新变量开辟独立的存储空间,并将原变量的具体数值完整拷贝一份放入其中。两者后续各自发生任何变更,互不影响。
  • 引用类型按引用(地址)复制:系统只是把栈中存储的堆内存指针地址复制了一份给新变量。新变量与旧变量持有的指针指向的是堆内存中同一个物理对象实体。通过任一方修改内部属性,另一方读取都会即时改变。
JavaScript 8种数据类型详解:基本类型与引用类型的区别
图 3:基本类型(按值独立)与引用类型(共享堆指针)在赋值与属性修改时的状态流转

3. 不可变性(Immutability)与可变性(Mutability)

基本数据类型的值无法在其自身的存储地址上发生内容改变。虽然代码中经常写 let a = 1; a = 2;,但这并非将数值 1 修改成了 2,而是直接将变量 a 指向了栈中保存数值 2 的新单元。对于字符串,无论通过数组下标修改还是调用方法,都不可能改变原字符串的字符。

相反,对象结构天然是可变的。你可以随时向对象追加新键、覆写已有属性或删除属性,其在堆内存中的基地址保持恒定。若要获得类似基本类型的“不可变”效果,需显式使用 Object.freeze() 进行浅层冻结,或引入不可变数据结构。

4. 比较机制(全等比较 ===)

  • 基本类型比较:比较的是两个值在字面上是否相同(严格比较数值与字符内容)。例如 "hello" === "hello" 判定为 true。
  • 引用类型比较:比较的是两个变量在栈中存储的堆内存指针地址是否相同,而非对比内部属性键值是否一致。即使两个独立创建的对象结构、属性、取值 100% 相同,由于它们分别在堆内存的不同地址开辟空间,其 === 比较结果永远是 false。
// 两个字面量对象在堆中开辟了两个独立的内存地址
const objA = { id: 1 };
const objB = { id: 1 };
console.log(objA === objB); // false!地址不同

const objC = objA;
console.log(objA === objC); // true!指向同一个堆内存块

四、数据类型判断:最佳路径决策树与 API 辨析

JavaScript 提供了多种类型判断手段,但各工具均存在明确的适用边界与缺陷。生产环境中盲目使用 typeof 经常导致隐藏严重的逻辑缺陷。

JavaScript 8种数据类型详解:基本类型与引用类型的区别
图 4:JavaScript 数据类型精准检测步骤与最佳决策路径

常见检测工具横向对比:

检测方法 适用范围 典型返回 / 表现 局限性与致命盲区
typeof 区分原始类型与函数 返回 8 个字符串之一 无法区分 null(返回 "object"),无法区分数组与普通对象
instanceof 检测对象的原型链关系 返回布尔值 true/false 无法判断原始类型字面量;跨 iframe 或多全局作用域时失效(原型对象不共享)
Array.isArray() 专门判断数组 true/false 仅针对数组,功能单一,但支持跨 iframe 检测
Object.prototype.toString 全类型精准识别 返回 "[object Type]" 语法较冗长,需要封装辅助工具函数
// 工业级高可靠类型获取函数示例
function getExactType(value) {
  if (value === null) return "null";
  if (typeof value !== "object" && typeof value !== "function") {
    return typeof value;
  }
  // 提取 [object Xxx] 内部的具体类型名称
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

console.log(getExactType(100n));             // "bigint"
console.log(getExactType(null));             // "null"
console.log(getExactType([1, 2, 3]));        // "array"
console.log(getExactType(new Date()));       // "date"
console.log(getExactType(/abc/g));           // "regexp"
console.log(getExactType(new Map()));        // "map"

五、工程实战避坑指南

1. 函数参数是“按值传递”还是“按引用传递”?

这是一个极具迷惑性的经典面试与工程问题:ECMAScript 规范中,所有函数的参数传递一律是“按值传递(Call by value)”。

很多开发者误以为传入对象是“按引用传递”,其实不然。当传入一个对象时,传递的值是该对象指针地址的一个拷贝副本。如果你在函数内部修改对象的内部属性,外部引用的对象自然被改动(因为两份指针副本寻址到同一个堆空间);但如果直接在函数内部将整个参数重新赋值为一个新对象,外部变量的原指向绝不会受任何影响:

function reassign(obj) {
  obj.name = "Modified"; // 影响外部对象:通过指针副本修改了堆数据
  obj = { name: "BrandNew" }; // 不影响外部:只是覆盖了本地指针副本的指向
}

const item = { name: "Original" };
reassign(item);
console.log(item.name); // 输出: "Modified",而不是 "BrandNew"!

2. 深拷贝(Deep Clone)与浅拷贝(Shallow Copy)的选择

由于引用类型的共享指针特性,简单的对象拷贝极易引发意外的状态污染:

  • 浅拷贝(Shallow Copy):使用展开运算符 { ...obj } 或 Object.assign({}, obj)。它仅复制第一层属性。如果属性值包含嵌套对象,嵌套对象依旧是共享引用的。
  • 现代标准深拷贝:现代浏览器与 Node.js 17+ 已经内置支持原生 structuredClone(obj),它原生支持循环引用、Date、RegExp、Map、Set 等各种类型,性能远胜传统的 JSON.parse(JSON.stringify(obj))(后者会丢失函数、undefined、Symbol、BigInt 并将 Date 降级为字符串)。

六、8 种数据类型全维度特性对比总结

下表为 8 种数据类型的全要素横向对照矩阵,直观展示每种类型的内存行为、分类归属与语法特征:

JavaScript 8种数据类型详解:基本类型与引用类型的区别
图 5:JavaScript 8 种数据类型全要素对比汇总一览表

掌握 JavaScript 数据类型分类及其底层的栈堆工作模型,是编写健壮前端与 Node.js 应用程序的基石。在日常编码中,对原始类型善用 typeof 快速判别,对可能存在的 null 保持敏感,对复杂引用类型坚持使用不可变原则或深拷贝隔离状态,能够杜绝大部分由副作用引起的内存泄露与数据异常隐患。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章