彻底搞懂 JS 数据类型:基本类型与引用类型的核心差异

86 阅读8分钟

彻底搞懂 JS 数据类型:基本类型与引用类型的核心差异

在 JavaScript 开发中,遇到过这样的困惑:同样是变量赋值,为什么修改一个变量,有的时候另一个变量不受影响,有的时候却会跟着变?其实这背后的核心原因,就在于 JavaScript 中两种数据类型——基本数据类型引用数据类型的不同存储与赋值机制。

这是 JS 中的基础核心知识点,也是理解后续原型链、闭包、深浅拷贝等概念的前提。本文将从存储本质、赋值行为、修改影响等维度,结合大量实例,彻底理清两者的差异,从此告别数据类型相关的“坑”。

一、先明确:JS 中的两种数据类型分类

JavaScript 中的数据类型分为两大类,这是 ES6 标准明确规定的,我们先清晰划分:

  • 基本数据类型(Primitive Types) :也叫原始类型,是不可再分的基本值。包括:NumberStringBooleannullundefinedSymbol(ES6 新增)、BigInt(ES10 新增)。
  • 引用数据类型(Reference Types) :也叫复杂类型,是由多个基本类型或其他引用类型组合而成的结构。包括:ObjectArrayFunctionDateRegExp 等所有非基本类型。

两者最核心的区别,在于在内存中的存储方式不同——这直接决定了它们的赋值行为和修改影响。

二、核心差异一:内存存储方式不同

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 仍为 10let 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

七、最后:一句话记住核心要点

基本类型存栈里,赋值复制值,变量独立;引用类型存堆里,赋值复制地址,变量共享;想避免共享,就用深拷贝。

如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,欢迎在评论区交流!