JS基本数据类型与引用类型的区别

作者:青柠

时间:2026-11-06

浏览:291

深入解析JavaScript基本数据类型与引用类型的核心区别,涵盖内存分配模型(栈与堆)、赋值与传参行为、严格相等比较机制以及类型检测的最佳实践。

文章目录
JS基本数据类型与引用类型的区别
导读图:JavaScript 基本数据类型与引用类型的核心机制概览

在 JavaScript 中,所有的数据可以划分为两大类:基本数据类型(Primitive Types)与引用数据类型(Reference Types)。它们在运行时的核心区别在于:基本数据类型直接将具体数值存储在栈内存中,赋值时产生独立的副本;而引用数据类型将对象实体存储在堆内存中,变量在栈内存中仅保存指向该对象的内存地址指针,赋值时仅拷贝指针本身,因而多个变量会共享同一个物理对象。

一、数据类型分类体系

ECMAScript 规范明确定义了语言支持的数据类型。理解两者的区别,首先需要明确哪些类型属于基本类型,哪些属于引用类型。

1. 7 种基本数据类型

JavaScript 目前共有 7 种基本数据类型(原始类型):

  • Number:双精度 64 位浮点数,以及特殊的 NaN 与 Infinity。
  • String:不可变的字符序列。
  • Boolean:仅有两个逻辑值:true 与 false。
  • Undefined:声明了变量但未赋值时的默认初始值。
  • Null:表示“空对象指针”或故意缺少对象值。
  • Symbol(ES6 新增):表示独一无二且不可变的唯一标识符。
  • BigInt(ES2020 新增):用于表示任意精度的超大整数。

2. 引用数据类型(Object 及其子类)

在 JavaScript 中,除了上述 7 种基本类型外,其余所有类型本质上都是引用类型。它们统一派生自根对象 Object,包括:

  • 普通对象(Object)
  • 数组(Array)
  • 函数(Function)
  • 内置工具对象:Date、RegExp、Math、Error 等
  • ES6 集合类型:Map、Set、WeakMap、WeakSet
JS基本数据类型与引用类型的区别
图 1:JavaScript 数据类型分类体系与运行特征全景图

二、底层存储机制:栈内存(Stack)与堆内存(Heap)

JavaScript 引擎(如 V8)在运行时划分了不同的内存区域,基本类型与引用类型的行为差异完全由底层的内存管理方式决定。

JS基本数据类型与引用类型的区别
图 2:栈内存与堆内存的物理模型及指针寻址关系

1. 栈内存(Stack)

栈内存是一块连续、大小固定、遵循“后进先出(LIFO)”原则的内存空间。它具有以下特点:

  • 存储内容:基本数据类型的实际值,以及引用类型变量在栈中的引用地址指针。
  • 存取效率:由于分配空间固定,栈指针上下移动即可完成分配与释放,读写速度极快。
  • 生命周期:与执行上下文(函数调用栈)紧密绑定。当一个函数执行完毕并出栈时,其栈帧内的局部变量会被操作系统自动回收。

2. 堆内存(Heap)

堆内存是一块结构松散、动态分配的大容量内存空间。它具有以下特点:

  • 存储内容:引用类型的完整对象实体(键值对、原型链、数组元素等)。
  • 存取机制:对象所占用的空间大小无法在编译期预知,且运行时可动态增删属性,因此必须在堆中按需分配。访问堆中的实体必须先在栈中读取 32 位或 64 位的指针,再通过指针解引用找到堆中的目标数据。
  • 生命周期:由垃圾回收机制(Garbage Collection)通过“可达性分析算法”自动检测并异步回收。当没有任何栈指针或外部属性引用该堆对象时,它才会被标记为垃圾并清理。
注意事项:闭包中的基本类型变量由于被外层函数引用,其生命周期超越了当前调用栈帧,V8 引擎会在编译期将逃逸的基本变量提升打包存入堆中的 Closure Context 对象中。但在语义模型上,它依旧保持基本类型的传值行为。

三、变量赋值与复制行为差异

内存存储形式的不同直接导致了两者在赋值(Copy)操作时的根本分歧:值拷贝与指针拷贝。

1. 基本类型的赋值:独立副本

将一个基本类型变量赋值给另一个变量时,引擎会在栈内存中开辟一块全新的独立空间,并将原有变量的值逐位复制进去:

let x = 10;
let y = x; // 栈中开辟新空间存放 y,其值为 10

y = 20;    // 修改 y 的值

console.log(x); // 输出: 10 (原变量 x 保持不变)
console.log(y); // 输出: 20

在这段代码中,x 与 y 在栈中各自持有属于自己的数值 10,对 y 的重新赋值完全不影响 x。

2. 引用类型的赋值:地址共享

将一个引用类型变量赋值给另一个变量时,复制的仅仅是栈中的内存地址指针,堆中的对象实体并未发生任何复制:

let user1 = { name: "Alice", score: 90 };
let user2 = user1; // 仅拷贝指针,user1 与 user2 指向堆中同一个对象

user2.score = 100; // 修改 user2 的属性

console.log(user1.score); // 输出: 100 (user1 的属性同步被修改)
console.log(user2.score); // 输出: 100
JS基本数据类型与引用类型的区别
图 3:引用类型变量赋值与属性修改的三步内存推导

3. 浅拷贝与深拷贝的本质

正是由于引用类型的赋值只是传递指针,日常开发中才需要使用拷贝技术:

  • 浅拷贝(Shallow Copy):如 Object.assign() 或扩展运算符 { ...obj }。它会创建一个新对象,但只复制对象自身的第一层属性。如果属性值是基本类型,则拷贝值;如果属性值是对象,拷贝的依然是指针。
  • 深拷贝(Deep Copy):如原生全局方法 structuredClone(obj) 或递归克隆。它会递归遍历目标对象的所有层级,在堆内存中完整克隆所有层级引用的全新实体,实现新旧对象的彻底解耦。

四、相等比较(===)与函数传参表现

1. 严格相等运算符(===)的比较规则

严格相等运算符在面对两种类型时,遵循不同的比对路径:

  • 基本类型:比较的是字面量数值。只要两者的类型一致且数值相等,即返回 true。
    console.log('hello' === 'hello'); // true
    console.log(100 === 100);         // true
  • 引用类型:比较的是物理指针地址,而不是对象的属性内容。哪怕两个对象包含一模一样的键值对,只要它们在堆中处于不同的地址,比较结果必然是 false。
    let objA = { count: 1 };
    let objB = { count: 1 };
    let objC = objA;
    
    console.log(objA === objB); // false (指向堆中两个不同的内存空间)
    console.log(objA === objC); // true  (指向同一个堆地址 0x...)

2. 函数参数传递:全部为“按值传递”

许多初学者容易产生误解,认为 JavaScript 在向函数传参时,对象是“按引用传递”的。事实上,ECMAScript 规范明确规定:所有函数的参数传递一律是“按值传递(Call by Value)”。

当向函数传递对象时,传递的值实际上是“该对象指针的副本”。请观察以下典型示例:

function modifyObject(param) {
  // 行为 1:通过指针修改属性
  param.title = "Updated";

  // 行为 2:重写形参指针
  param = { title: "New Object" };
}

let article = { title: "Original" };
modifyObject(article);

console.log(article.title); // 输出: "Updated"

原因解析:

  1. 调用 modifyObject(article) 时,形参 param 在自身的局部栈帧中接收了 article 指针的拷贝。两者都指向同一个堆地址。
  2. 执行 param.title = "Updated" 时,代码沿指针修改了该堆地址上的属性,因此外部 article.title 生效。
  3. 执行 param = { ... } 时,是在堆中新建了一个对象,并将 param 局部变量的指针重定向到了新地址。此时外部 article 栈中的指针未受任何影响,依然指向原对象。

五、不可变性(Immutable)与可变性(Mutable)

基本数据类型的值具备物理上的不可变性(Immutability)。一旦一个原始值被创建,它就永远无法在内存中被修改。所有针对基本类型的操作,本质上都是返回一个全新的值:

let str = "javascript";
str.toUpperCase();
console.log(str); // 输出: "javascript" (原字符串未被改变)

str[0] = "J";     // 试图修改原始字符,静默失败(严格模式下报错)
console.log(str); // 仍为 "javascript"

// 尝试给基本类型添加属性
let num = 123;
num.desc = "number type"; // JS 会创建临时的包装对象并在本行结束时立即销毁
console.log(num.desc);    // 输出: undefined

相对地,引用类型具有高度的动态可变性(Mutability)。无需重新分配变量,可以直接向堆对象追加、更新或使用 delete 剔除属性。

六、类型检测的最佳实践与陷阱

两类数据在进行类型判定时适用的操作符各有侧重:

1. typeof 运算符

typeof 是检测基本类型的首选工具,但有两大必须留意的语言规范特例:

  • typeof null === 'object':这是 JavaScript 诞生早期的设计历史遗留缺陷。当时底层存储将低 3 位全为 0 判定为 object,而 null 在机器码中表示为空指针(全 0),导致误判。判断 null 请使用严格相等:val === null。
  • typeof function() {} === 'function':函数虽属于引用类型,但语言规范将其单独赋予了 'function' 字符串标识。

2. instanceof 运算符

instanceof 专门用于引用类型,通过检测构造函数的 prototype 属性是否存在于目标实例的原型链上。注意它不能可靠检测基本类型:

console.log([] instanceof Array);        // true
console.log([] instanceof Object);       // true
console.log("hello" instanceof String);  // false (字面量不是构造包装实例)

3. Object.prototype.toString.call() 全能方案

如果需要准确识别出具体是哪一种类型(无论基本类型还是特定引用对象),最通用的标准化方案是借用原型方法:

function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1);
}

console.log(getType(123));         // "Number"
console.log(getType(null));        // "Null"
console.log(getType(undefined));   // "Undefined"
console.log(getType([]));          // "Array"
console.log(getType(/abc/g));      // "RegExp"
console.log(getType(new Map()));   // "Map"

七、核心区别全维度总结

下表概括了 JavaScript 基本数据类型与引用类型在各层面的关键不同:

比较维度 基本数据类型 (Primitive) 引用数据类型 (Reference)
包含成员 Number, String, Boolean, Null, Undefined, Symbol, BigInt Object, Array, Function, Date, RegExp, Map, Set 等
内存存储 栈内存(占用固定空间,直接保存值) 堆内存(动态空间存实体) + 栈内存(存地址指针)
变量赋值 深层值拷贝,开辟新空间,变量完全相互独立 仅拷贝栈内指针,新旧变量指向堆中同一物理实例
比较规则 (===) 比较具体数值(值相等即为 true) 比较指针地址(内存地址相同才为 true)
值可变性 物理不可变(Immutable),无法新增属性 动态可变(Mutable),可随意增删修改属性
函数传参 按值传递(传递数值副本) 按值传递(传递指针地址副本)
推荐检测手段 typeof(排查 null 用 === null) instanceof 或 Object.prototype.toString.call()
JS基本数据类型与引用类型的区别
图 4:基本数据类型与引用数据类型全维度对照矩阵

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章