浅拷贝、深拷贝与引用赋值

在处理引用类型(对象、数组)时,「引用赋值」「浅拷贝」「深拷贝」是核心概念,其本质差异体现在内存地址的指向关系上。

一、引用赋值:非拷贝操作,共享内存地址

引用赋值是将引用类型的内存地址(指针)赋值给变量,未创建任何新的对象或数组,赋值后的变量与原变量指向同一块内存空间。对任一变量的修改,都会直接作用于共享的内存数据,进而影响所有指向该地址的变量。

1. 对象的引用赋值

变量存储对象的内存地址,赋值后两变量指向同一对象内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
// 原对象在堆内存中占据独立空间,内存地址记为 0x001
const obj1 = { id: 1, nested: { value: 10 } };
// 引用赋值:obj2 存储的指针指向 0x001,与 obj1 共享同一对象
const obj2 = obj1;

// 修改 obj2 的顶层属性:操作 0x001 内存空间的数据
obj2.id = 2;
console.log(obj1.id); // 输出 2(obj1 指向的 0x001 数据被修改)

// 修改 obj2 的嵌套对象属性:同样操作 0x001 内嵌套对象的内存空间(记为 0x002)
obj2.nested.value = 20;
console.log(obj1.nested.value); // 输出 20(0x002 数据被修改)

2. 数组的引用赋值

变量存储数组的内存地址,赋值后两变量指向同一数组内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
// 原数组在堆内存中占据独立空间,内存地址记为 0x003
const arr1 = [1, [2, 3]];
// 引用赋值:arr2 存储的指针指向 0x003,与 arr1 共享同一数组
const arr2 = arr1;

// 修改 arr2 的顶层元素:操作 0x003 内存空间的数据
arr2[0] = 10;
console.log(arr1[0]); // 输出 10(0x003 数据被修改)

// 修改 arr2 的嵌套数组元素:操作 0x003 内嵌套数组的内存空间(记为 0x004)
arr2[1][0] = 20;
console.log(arr1[1][0]); // 输出 20(0x004 数据被修改)

核心结论

引用赋值的本质是「指针复用」:变量仅存储引用类型的内存地址,未创建新内存空间,因此所有指向同一地址的变量会共享数据修改结果。

二、浅拷贝:创建新容器,嵌套引用共享地址

浅拷贝会为对象或数组创建新的顶层内存空间(新对象/新数组),但对嵌套的引用类型(如子对象、子数组)仅复制其内存地址,未创建新的嵌套内存空间。即顶层数据独立,嵌套数据共享。

1. 对象的浅拷贝实现与特性

常用实现方式:Object.assign({}, obj)、扩展运算符 { ...obj }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原对象:顶层地址 0x005,嵌套对象地址 0x006
const objOrigin = { a: 1, nestedObj: { b: 2 } };

// 浅拷贝:创建新对象(顶层地址 0x007),嵌套对象仍指向 0x006
const objShallow = { ...objOrigin };
// 或 const objShallow = Object.assign({}, objOrigin);

// 1. 修改顶层基本类型属性:操作 0x007 内存空间,不影响 0x005
objShallow.a = 10;
console.log(objOrigin.a); // 输出 1(0x005 数据未变)

// 2. 修改嵌套对象属性:操作 0x006 内存空间,影响 objOrigin
objShallow.nestedObj.b = 20;
console.log(objOrigin.nestedObj.b); // 输出 20(0x006 数据被修改)

2. 数组的浅拷贝实现与特性

常用实现方式:Array.from(arr)、扩展运算符 [ ...arr ]arr.slice()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原数组:顶层地址 0x008,嵌套数组地址 0x009
const arrOrigin = [1, [2, 3]];

// 浅拷贝:创建新数组(顶层地址 0x00A),嵌套数组仍指向 0x009
const arrShallow = [ ...arrOrigin ];
// 或 const arrShallow = Array.from(arrOrigin); / arrOrigin.slice()

// 1. 修改顶层元素:操作 0x00A 内存空间,不影响 0x008
arrShallow[0] = 10;
console.log(arrOrigin[0]); // 输出 1(0x008 数据未变)

// 2. 修改嵌套数组元素:操作 0x009 内存空间,影响 arrOrigin
arrShallow[1][0] = 20;
console.log(arrOrigin[1][0]); // 输出 20(0x009 数据被修改)

核心结论

浅拷贝的核心是「顶层独立,嵌套共享」:新对象/数组拥有独立顶层内存地址,但嵌套引用类型仍复用原内存地址,因此嵌套数据的修改会同步影响原引用类型。

三、深拷贝:全层级独立,无共享内存地址

深拷贝会递归复制引用类型的所有层级,为顶层及嵌套的每一个引用类型(对象、数组)都创建新的内存空间。新对象/数组与原引用类型完全独立,不存在任何内存地址共享,修改任一数据均不影响对方。

1. 对象的深拷贝实现与特性

常用实现方式:JSON.parse(JSON.stringify(obj))(基础场景)、第三方库(如 lodash _.cloneDeep)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 原对象:顶层地址 0x00B,嵌套对象地址 0x00C
const objOrigin = { x: 1, nested: { y: 2 } };

// 深拷贝:创建新顶层对象(0x00D),并递归创建新嵌套对象(0x00E)
const objDeep = JSON.parse(JSON.stringify(objOrigin));

// 1. 修改顶层属性:操作 0x00D,不影响 0x00B
objDeep.x = 10;
console.log(objOrigin.x); // 输出 1(0x00B 数据未变)

// 2. 修改嵌套对象属性:操作 0x00E,不影响 0x00C
objDeep.nested.y = 20;
console.log(objOrigin.nested.y); // 输出 2(0x00C 数据未变)

2. 数组的深拷贝实现与特性

同对象实现逻辑,JSON.parse(JSON.stringify(arr)) 可直接用于数组深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原数组:顶层地址 0x00F,嵌套数组地址 0x010
const arrOrigin = [1, [2, 3], { z: 4 }];

// 深拷贝:创建新顶层数组(0x011),递归创建新嵌套数组(0x012)和新对象(0x013)
const arrDeep = JSON.parse(JSON.stringify(arrOrigin));

// 1. 修改顶层元素:操作 0x011,不影响 0x00F
arrDeep[0] = 10;
console.log(arrOrigin[0]); // 输出 1(0x00F 数据未变)

// 2. 修改嵌套数组:操作 0x012,不影响 0x010
arrDeep[1][0] = 20;
console.log(arrOrigin[1][0]); // 输出 2(0x010 数据未变)

// 3. 修改嵌套对象:操作 0x013,不影响原嵌套对象地址
arrDeep[2].z = 40;
console.log(arrOrigin[2].z); // 输出 4(原嵌套对象数据未变)

核心结论

深拷贝的核心是「全层级独立」:新引用类型及其所有嵌套引用类型均拥有独立内存地址,与原引用类型无任何内存共享,数据修改完全隔离。

四、核心疑难点解答

问:{ ...obj }[ ...arr ] 已创建新对象/数组,为何修改仍影响原数据?

{ ...obj }[ ...arr ] 仅为顶层创建新内存地址,属于浅拷贝。对于嵌套的引用类型(如 obj.nestedarr[1]),新对象/数组仍存储原嵌套类型的内存地址,未创建新嵌套内存空间。因此,修改嵌套数据时,操作的是原嵌套类型的内存地址,会同步影响原对象/数组。

问:let ref = obj1 后执行 ref = obj2obj1 会变为 obj2 吗?

不会。ref 作为变量,存储的是引用类型的内存地址(指针)。初始 ref = obj1 时,ref 指向 obj1 的内存地址(如 0x001);执行 ref = obj2 后,ref 的指针仅从 0x001 切换为 obj2 的内存地址(如 0x002),并未修改 obj1 所在内存空间(0x001)的数据。同理,数组场景 let ref = arr1; ref = arr2 中,arr1 的内存数据也不会变化。

问:引用赋值、浅拷贝、深拷贝的内存地址关系差异?

操作类型 对象/数组顶层地址 嵌套引用类型地址 修改嵌套数据影响原数据?
引用赋值 与原数据相同 与原数据相同
浅拷贝 与原数据不同 与原数据相同
深拷贝 与原数据不同 与原数据不同

五、其他语言的概念一致性

「浅拷贝」「深拷贝」的核心定义在所有支持引用类型的语言中一致:浅拷贝仅复制顶层,嵌套引用共享内存;深拷贝递归复制所有层级,全内存独立。差异仅体现在实现方式,且均需兼顾对象(如 Java 类实例、Python 字典)与数组(如 Java 数组、Python 列表):

语言 浅拷贝实现(对象/数组) 深拷贝实现(对象/数组)
TypeScript 对象:{ ...obj }/Object.assign;数组:[ ...arr ] 对象/数组:JSON.parse(JSON.stringify())/_.cloneDeep
Java 对象:clone()(默认);数组:Arrays.copyOf() 对象/数组:重写 clone() 递归拷贝/序列化(如 ObjectOutputStream
Python 对象:copy.copy();数组(列表):copy.copy() 对象/数组:copy.deepcopy()
C# 对象:MemberwiseClone();数组:Array.Copy() 对象/数组:序列化(如 BinaryFormatter)/手动递归

六、总结

  1. 引用赋值是指针复用,未创建新内存空间,所有变量共享同一引用类型数据;
  2. 浅拷贝为顶层创建新内存空间,但嵌套引用类型仍共享原地址,仅顶层数据独立;
  3. 深拷贝递归创建全层级新内存空间,新引用类型与原数据完全独立,无任何共享;
  4. 所有语言中,浅拷贝与深拷贝的核心定义一致,差异仅在实现方式,需同时关注对象与数组的嵌套场景。

掌握三者的内存地址关系,是避免引用类型操作Bug的关键,也是后续学习复杂数据结构(如树形结构)的基础。


浅拷贝、深拷贝与引用赋值
https://royrao-blog.vercel.app/数据结构与算法/shallow-clone-and-deep-clone/
作者
Roy Rao
发布于
2025年09月25日 上午
许可协议