彻底搞懂 JS 数据类型:基本类型与引用类型的核心差异
在 JavaScript 开发中,遇到过这样的困惑:同样是变量赋值,为什么修改一个变量,有的时候另一个变量不受影响,有的时候却会跟着变?其实这背后的核心原因,就在于 JavaScript 中两种数据类型——基本数据类型和引用数据类型的不同存储与赋值机制。
这是 JS 中的基础核心知识点,也是理解后续原型链、闭包、深浅拷贝等概念的前提。本文将从存储本质、赋值行为、修改影响等维度,结合大量实例,彻底理清两者的差异,从此告别数据类型相关的“坑”。
一、先明确:JS 中的两种数据类型分类
JavaScript 中的数据类型分为两大类,这是 ES6 标准明确规定的,我们先清晰划分:
- 基本数据类型(Primitive Types) :也叫原始类型,是不可再分的基本值。包括:
Number、String、Boolean、null、undefined、Symbol(ES6 新增)、BigInt(ES10 新增)。 - 引用数据类型(Reference Types) :也叫复杂类型,是由多个基本类型或其他引用类型组合而成的结构。包括:
Object、Array、Function、Date、RegExp等所有非基本类型。
两者最核心的区别,在于在内存中的存储方式不同——这直接决定了它们的赋值行为和修改影响。
二、核心差异一:内存存储方式不同
JavaScript 运行时,内存主要分为两个区域:栈内存(Stack) 和堆内存(Heap) 。两种数据类型分别存储在这两个区域,这是所有差异的根源。
2.1 基本数据类型:直接存储在栈内存
基本数据类型的特点是“体积小、结构简单”,所以 JS 引擎会直接把它们的值存储在栈内存中。变量名直接指向栈内存中的具体值,不存在“引用”的概念。
我们用一个简单的示意图理解:
变量 x → 栈内存 → 值:30
举个代码示例:
// 基本数据类型:Number
var x = 30;
// 变量 y 赋值为 x 的值
var y = x;
此时内存中的状态是:栈内存中会创建两个独立的空间,分别存储 30(对应 x)和 30(对应 y)。x 和 y 各自指向自己的栈内存空间,互不干扰。
2.2 引用数据类型:值存储在堆内存,引用存储在栈内存
引用数据类型的特点是“体积大、结构复杂”(比如一个包含多个属性的对象、一个长度很长的数组)。如果把它们直接存储在栈内存,会导致栈内存占用过大,影响程序运行效率。
因此 JS 引擎采用了“分离存储”的策略:
- 把引用类型的实际值存储在堆内存中;
- 把堆内存中该值的内存地址(引用) 存储在栈内存中;
- 变量名指向栈内存中的“引用地址”,通过这个地址才能找到堆内存中的实际值。
示意图理解:
变量 x → 栈内存 → 引用地址:0x123 → 堆内存 → 实际值:{ m: 1 }
代码示例:
// 引用数据类型:Object
var x = { m: 1 };
// 变量 y 赋值为 x 的引用地址
var y = x;
此时内存中的状态是:堆内存中创建一个空间存储对象 { m: 1 },栈内存中 x 和 y 存储的都是同一个引用地址(比如 0x123),这个地址都指向堆内存中的同一个对象。
三、核心差异二:赋值行为与修改影响不同
基于不同的存储方式,两种数据类型的赋值操作和修改操作,会产生完全不同的结果。这也是开发中最容易遇到“坑”的地方。
3.1 基本数据类型:赋值是“值的复制”,变量间互相独立
基本数据类型的赋值,本质是把栈内存中的“实际值”复制一份,赋值给新的变量。复制完成后,两个变量拥有各自独立的值,修改其中一个,绝对不会影响另一个。
// 基本数据类型:Number
var x = 30;
// 赋值:把 x 的值 30 复制给 y
var y = x;
// 修改 x 的值
x += 10; // x 变成 40
// 查看 y 的值:不受影响
console.log(y); // 输出 30
再看一个 String 类型的例子,结果一致:
var str1 = "hello";
var str2 = str1;
str1 = str1 + " world"; // str1 变成 "hello world"
console.log(str2); // 输出 "hello",不受影响
这里要注意:虽然 JS 中的 String 是不可变的(无法直接修改字符串的某一位),但赋值行为依然遵循基本类型的“值复制”规则,变量间互相独立。
3.2 引用数据类型:赋值是“引用的复制”,变量间指向同一对象
引用数据类型的赋值,本质是把栈内存中的“引用地址”复制一份,赋值给新的变量。复制完成后,两个变量拥有同一个引用地址,都指向堆内存中的同一个对象。
因此,通过任意一个变量修改对象的属性,都会影响到所有引用该对象的变量——因为它们操作的是同一个堆内存中的对象。
// 引用数据类型:Object
var x = { m: 1 };
// 赋值:把 x 的引用地址复制给 y
var y = x;
// 通过 x 修改对象属性
x.m++; // 对象的 m 属性变成 2
// 查看 y.m:受影响,因为 x 和 y 指向同一个对象
console.log(y.m); // 输出 2
// 反过来,通过 y 修改对象属性,x 也会受影响
y.m = 10;
console.log(x.m); // 输出 10
数组作为引用类型,表现也是一样的:
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1.push(4); // 给 arr1 新增元素
console.log(arr2); // 输出 [1, 2, 3, 4],arr2 也跟着变
四、为什么要设计成这样?—— 性能优化的考量
可能会有疑问:为什么不把引用类型也直接存储在栈内存,搞成“值复制”呢?这其实是 JS 引擎的性能优化策略。
- 对于基本类型:体积小、结构简单,值复制的开销非常低,直接存在栈内存中,访问速度快;
- 对于引用类型:体积可能很大(比如一个包含上千个属性的对象),如果每次赋值都复制整个对象,会占用大量的内存空间,而且复制过程会消耗很多时间,严重影响程序性能。而复制一个小小的引用地址,开销几乎可以忽略不计。
简单来说,这种设计是“空间”与“时间”的权衡——用“引用地址”的方式,减少内存占用,提升程序运行效率。
五、关键总结:一张表理清所有差异
为了方便大家记忆,我们把两种数据类型的核心差异整理成表格:
| 对比维度 | 基本数据类型 | 引用数据类型 |
|---|---|---|
| 存储位置 | 栈内存(直接存储值) | 堆内存存储值,栈内存存储引用地址 |
| 赋值行为 | 复制实际值,生成独立副本 | 复制引用地址,共享同一个对象 |
| 变量间关系 | 互相独立,互不影响 | 指向同一对象,修改互相影响 |
| 修改方式 | 直接修改变量的值(本质是重新赋值) | 通过引用地址修改堆内存中对象的属性 |
| 典型示例 | let a = 10; let b = a; a = 20; b 仍为 10 | let obj1 = {x:1}; let obj2 = obj1; obj1.x=2; obj2.x 为 2 |
六、实用扩展:如何避免引用类型的“共享”问题?
在实际开发中,我们有时不希望多个变量共享同一个对象——比如我们想基于一个现有对象创建一个新对象,修改新对象时不影响原对象。这时候就需要用到深拷贝(Deep Copy) 技术。
深拷贝的核心是:创建一个全新的对象,把原对象的所有属性(包括嵌套属性)都复制到新对象中,新对象和原对象完全独立,修改其中一个不会影响另一个。
这里给大家提供两种常用的深拷贝方案:
6.1 简单场景:JSON 序列化/反序列化
适合没有函数、Date、RegExp 等特殊属性的普通对象/数组:
var obj1 = { m: 1, arr: [1, 2, 3] };
// 深拷贝:先序列化成 JSON 字符串,再反序列化成新对象
var obj2 = JSON.parse(JSON.stringify(obj1));
// 修改 obj2 的属性和数组
obj2.m = 10;
obj2.arr.push(4);
// 原对象 obj1 不受影响
console.log(obj1); // 输出 { m: 1, arr: [1, 2, 3] }
console.log(obj2); // 输出 { m: 10, arr: [1, 2, 3, 4] }
6.2 通用场景:递归实现深拷贝
适合包含函数、Date 等特殊属性的对象,兼容性更强:
// 递归深拷贝函数
function deepClone(target) {
// 先判断是否是基本类型或 null,直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
// 初始化新对象/新数组
let cloneTarget = Array.isArray(target) ? [] : {};
// 遍历原对象的所有属性,递归复制
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key]);
}
}
return cloneTarget;
}
// 测试:包含函数的对象
var obj1 = {
m: 1,
fn: function() { console.log('hello'); },
arr: [1, 2, 3]
};
var obj2 = deepClone(obj1);
obj2.m = 10;
obj2.arr.push(4);
console.log(obj1.m); // 1(不受影响)
console.log(obj1.arr); // [1,2,3](不受影响)
obj1.fn(); // hello(函数属性也能正常复制)
obj2.fn(); // hello
七、最后:一句话记住核心要点
基本类型存栈里,赋值复制值,变量独立;引用类型存堆里,赋值复制地址,变量共享;想避免共享,就用深拷贝。
如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,欢迎在评论区交流!