JavaScript 引用赋值、浅拷贝与深拷贝完全解析
本文深入解析JavaScript中引用赋值、浅拷贝和深拷贝的核心区别。通过栈内存与堆内存机制,阐明基本类型与引用类型的存储差异。详细对比三种拷贝方式对嵌套对象的影响,并列举扩展运算符、Object.assign等浅拷贝方法及JSON序列化、递归等深拷贝实现,帮助开发者避免数据修改引发的Bug。
前言
如果在日常开发中,你经常混淆引用赋值、浅拷贝和深拷贝的概念,难以区分它们的差异,并因此频繁遇到数据被意外修改或拷贝不彻底导致的Bug,那么这篇文章正是为你准备的。本文将使用通俗易懂的语言、直观的图示以及丰富的代码示例,帮助你一次性彻底掌握这些概念。在深入探讨具体的拷贝机制之前,我们需要先回顾JavaScript的数据类型分类及其在内存中的存储原理。
一、基础概念铺垫
1. JavaScript 数据类型分类
- 基本数据类型包括:字符串(String)、数字(Number)、布尔值(Boolean)、空值(Null)、未定义(Undefined)、Symbol 和 BigInt。
- 引用数据类型主要包括:对象(Object)、数组(Array)和函数(Function)。此外,还有一些特殊的内置对象,如正则表达式(RegExp)、日期(Date)、Map、Set,以及 Promise、Error 等。
2. 内存存储机制核心原理
JavaScript 引擎将内存划分为栈内存(Stack)和堆内存(Heap)。不同类型的数据会被存储在不同的内存区域中,嵌套引用的处理依然遵循以下规则。
| 特性 | 栈内存 (Stack) | 堆内存 (Heap) |
|---|---|---|
| 存储内容 | 基本数据类型值、引用类型的指针 | 引用数据类型的实际内容 |
| 数据结构 | 后进先出 (LIFO) | 动态分配的树/图结构 |
| 分配方式 | 连续内存,自动分配 | 随机内存,动态分配 |
| 访问速度 | 极快(直接CPU访问) | 较慢(通过指针间接访问) |
| 大小限制 | 小(通常1-8MB) | 大(可达GB级) |
| 生命周期 | 函数/块作用域结束自动释放 | 由垃圾回收器(GC)管理 |

如上图所示,两种数据类型的内存访问流程如下:
- 基本数据类型的变量直接从栈内存中获取数据。
- 引用数据类型的变量访问过程:
- 读取栈内存中的指针(地址)。
- 通过指针找到堆内存中存储的实际数据。
二、不同类型的拷贝行为
1. 基本数据类型的拷贝
基本数据类型的拷贝过程非常简单。由于它们直接存储在栈内存中,拷贝时直接复制值本身,不涉及引用关系。
let a = 10;
let b = a; // 直接复制值
b = 20;
console.log(a); // 10(不受 b 修改影响)
console.log(b); // 20
2. 引用数据类型的拷贝
方式一:引用赋值(非拷贝)
对引用数据类型进行赋值时,默认执行的是引用赋值,即复制指向堆内存的指针地址,而非复制实际内容。这意味着两个变量将指向堆内存中的同一个对象。因此,修改其中一个变量,另一个也会受到影响。
let a = [1, 2, 5];
let b = a; // 引用赋值(复制指针)
a[1] = 4; // 修改 a 指向的数组
console.log(a); // [1, 4, 5]
console.log(b); // [1, 4, 5](b 也受影响)
console.log(a === b); // true(指向同一个对象)

方式二: 浅拷贝
浅拷贝是针对引用类型的一种拷贝方式,它会创建一个新对象,但仅复制对象的第一层属性。其具体规则如下:
- 对于基本类型属性:直接复制值。
- 对于引用类型属性:仅复制指针,不复制指向的对象本身。
示例1:对数组 a = [1, 2, [3, 4], 5] 进行浅拷贝得到 b 后:
b[0]、b[1]、b[3]是基本类型,修改它们不会影响ab[2]是引用类型(数组),修改b[2]会同时影响a[2],因为它们指向同一个子数组
const a = [1, 2, [3, 4], 5];
const b = [...a]; // 浅拷贝
a[0] = 100;
console.log(b[0]); // 1(不受影响,基本类型独立)
a[2][1] = 400; // 修改子数组元素
console.log(b[2][1]); // 400(受影响,共享子数组引用)
示例2:
// 原始对象
const a = {
name: "alice", // 基本类型(栈内存存储值)
profile: { // 引用类型(堆内存存储对象,栈内存存储指针)
age: 25,
city: "beijing"
}
};
// 使用扩展运算符进行浅拷贝
const b = { ...a };
// 修改浅拷贝对象
b.name = "bob";
b.profile.age = 30;
b.profile.city = "shanghai";
// 查看结果
console.log("原始对象 a.name:", a.name);
// 输出:"alice"(基本类型值独立,不受影响)
console.log("原始对象 a.profile.age:", a.profile.age);
// 输出:30(引用类型共享堆内存,被修改)
console.log("原始对象 a.profile.city:", a.profile.city);
// 输出:"shanghai"(引用类型共享堆内存,被修改)
console.log("a.profile === b.profile:", a.profile === b.profile);
// 输出:true(两者指向堆中同一个对象)
常见的浅拷贝方法:
- 扩展运算符(推荐)
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
- Object.assign()
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
- 数组的 slice()、concat() 方法
const arr = [1, 2, { a: 3 }];
const shallowCopy1 = arr.slice();
const shallowCopy2 = [].concat(arr);
- Array.from()
const arr = [1, 2, { a: 3 }];
const shallowCopy = Array.from(arr);
方式三、深拷贝
深拷贝会创建一个全新的对象,完全复制原始对象的所有层级属性,包括嵌套的引用类型,使得新旧对象完全独立。
实现方式 1:JSON 方法(最常用但有局限)
// 原始对象
const a = {
name: "alice", // 基本类型(栈内存存储值)
profile: { // 引用类型(堆内存存储对象)
age: 25,
city: "beijing"
}
};
// 实现深拷贝
const deepCopy = JSON.parse(JSON.stringify(a));
// 修改深拷贝对象的属性
deepCopy.name = "bob"; // 修改基本类型
deepCopy.profile.age = 30; // 修改嵌套引用类型
deepCopy.profile.city = "shanghai";
// 查看结果对比
console.log("原始对象 a.name:", a.name);
// 输出:"alice"(基本类型不受影响)
console.log("原始对象 a.profile.age:", a.profile.age);
// 输出:25(嵌套引用类型也不受影响)
console.log("原始对象 a.profile.city:", a.profile.city);
// 输出:"beijing"(嵌套引用类型完全独立)
console.log("a.profile === deepCopy.profile:", a.profile === deepCopy.profile);
// 输出:false(两者指向堆中不同对象)
注意限制:
- 无法复制函数、undefined、Symbol
- 日期对象会被转换为字符串
- 无法处理循环引用
- 会丢失原型链信息
实现方式 2:递归实现(自定义深拷贝函数)
由于递归实现较为复杂,这里不展开详细代码,但基本原理是遍历对象的所有属性,对引用类型属性递归调用拷贝函数,直到所有层级都被复制。
三、深浅拷贝对比总结
| 类型 | 引用类型 | 内存地址 | 第一层修改 | 第二层修改 |
|---|---|---|---|---|
| 引用赋值 | 引用复制 | 相同 | 相互影响 | 相互影响 |
| 浅拷贝 | 仅第一层值复制,嵌套层引用复制 | 不同 | 独立 | 相互影响 |
| 深拷贝 | 完全复制 | 不同 | 独立 | 独立 |
深浅拷贝的核心区别在于对嵌套引用类型的处理方式,这直接决定了拷贝后对象的独立性:
- 引用赋值:本质上不是拷贝,只是复制了对象的引用指针。两个变量共享同一块堆内存,任何层级的修改都会相互影响,内存地址相同。
- 浅拷贝:创建新的内存地址存储对象,但仅对第一层属性进行值复制。对于基本类型属性,修改后彼此独立;但对于嵌套的引用类型属性,仍共享原始引用,修改会相互影响。
- 深拷贝:完全创建新的对象,递归复制所有层级的属性(包括嵌套引用类型)。新旧对象拥有完全独立的内存空间,任何层级的修改都不会相互影响,内存地址不同。
到此这篇关于JavaScript中引用赋值、浅拷贝和深拷贝的文章就介绍到这了,更多相关JS引用赋值、浅拷贝和深拷贝内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
