图解对象之:深拷贝与浅拷贝

对象拷贝,引用

对象与原始类型其中一个基本的区别是:对象“通过引用的形式”被存储和拷贝。

原始类型值:字符串,数字,布尔值 —— 被“作为整体”赋值/拷贝。

例如:

let message = "Hello!";
let phrase = message;

结果我们就有了两个独立的变量,每个都存储着字符串 "Hello!"

对象不是这样的。

变量存储的不是对象自身,而是该对象的“内存地址”,换句话说就是一个对该对象的“引用”。

下面是这个对象的示意图:

let user = {
  name: "John"
};

这里,该对象被存储在内存中的某处。而变量 user 保存的是对此处的“引用”。

当一个对象变量被拷贝 —— 引用则被拷贝,而该对象并没有被复制。

例如:

let user = { name: "John" };

let admin = user; // 拷贝引用

现在我们有了两个变量,它们保存的都是对同一个对象的引用:

我们可以用任何变量来访问该对象并修改它的内容:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // 通过 "admin" 引用来修改

alert(user.name); // 'Pete',修改能通过 "user" 引用看到

上面的例子说明这里只有一个对象。就像我们有个带两把钥匙的锁柜,并使用其中一把钥匙(admin)来打开它。那么,我们如果之后用另外一把钥匙(user),就也能看到所作的改变。

通过引用来比较

对于对象来说,普通相等 == 和严格相等 === 是两个作用结果完全一样的运算符。

仅当两个对象为同一对象时,两者才相等。

这里两个变量都引用同一个对象,所以它们相等:

let a = {};
let b = a; // 拷贝引用

alert( a == b ); // true,都引用同一对象
alert( a === b ); // true

而这里两个独立的对象则并不相等,即使它们都为空:

let a = {};
let b = {}; // 两个独立的对象

alert( a == b ); // false

对于类似 obj1 > obj2 的比较,或者跟一个原始类型值的比较 obj == 5,对象都会被转换为原始值。我们很快就会学到对象是如何转换的,但是说实话,类似的比较很少出现,通常是在编程错误的时候才会出现这种情况。

克隆与合并,Object.assign

那么,拷贝一个对象变量会又创建一个对相同对象的引用。

但是,如果我们想要复制一个对象,那该怎么做呢?创建一个独立的拷贝,克隆?

这也是可行的,但稍微有点困难,因为 JavaScript 没有提供对此操作的内建的方法。实际上,也很少需要这样做。通过引用进行拷贝在大多数情况下已经很好了。

但是,如果我们真的想要这样做,那么就需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。

就像这样:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据

alert( user.name ); // 原来的对象中的 name 属性依然是 John

我们也可以使用 Object.assign[1] 方法来达成同样的效果。

语法是:

Object.assign(dest, [src1, src2, src3...])
  • 第一个参数 dest 是指目标对象。

  • 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。

  • 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。

  • 调用结果返回 dest

例如,我们可以用它来合并多个对象:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);

// 现在 user = { name: "John", canView: true, canEdit: true }

如果被拷贝的属性的属性名已经存在,那么它会被覆盖:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // 现在 user = { name: "Pete" }

我们也可以用 Object.assign 代替 for..in 循环来进行简单克隆:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。

当然,我们还可以用展开运算符 let clone = { ...obj }; 或者普通的 for...in 循环来实现浅拷贝。

深层克隆

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?

例如:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 sizes:

就像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 分享同一个 sizes
user.sizes.width++;       // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果

为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

这里有一个标准的深拷贝算法,它不仅能处理上面的例子,还能应对更多复杂的情况,它被称为 结构化拷贝算法[2]

我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如 JavaScript 库 lodash[3] 中的 _.cloneDeep(obj)[4]

总结

对象通过引用被赋值和拷贝。换句话说,一个变量存储的不是“对象的值”,而是一个对值的“引用”(内存地址)。因此,拷贝此类变量或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。

所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。

为了创建“真正的拷贝”(一个克隆),我们可以使用 Object.assign 来做所谓的“浅拷贝”(嵌套对象被通过引用进行拷贝)或者使用“深拷贝”函数,例如 _.cloneDeep(obj)[5]

参考资料

[1]

Object.assign: https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

[2]

结构化拷贝算法: https://html.spec.whatwg.org/multipage/structured-data.html#safe-passing-of-structured-data

[3]

lodash: https://lodash.com

[4]

_.cloneDeep(obj): https://lodash.com/docs#cloneDeep

[5]

_.cloneDeep(obj): https://lodash.com/docs#cloneDeep

看完三件事

如果你觉得本文对你有帮助,我想请你帮个忙:

  1. 转发本文点赞或者点个「在看」,是对我最大的认可和支持;

  2. 关注公众号「技术漫谈」,订阅更多精彩内容,获取更多学习资料;

  3. 公众号后台回复「加群」,加入算法和技术交流群,与更多读者交流。


 

长按上方二维码关注公众号「技术漫谈」,订阅更多精彩内容。

“在看”我吗?

<think> 首先,用户要求了解深拷贝浅拷贝的详细介绍。我需要基于系统指令和用户的消息来构建回答。 系统指令: - 所有行内数学表达式必须使用$...$格式。但在这个主题中,可能没有数学表达式,所以我可以忽略如果不需要。 - 独立公式使用$$...$$单独成段。同样,可能不适用。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 添加引用标识在引用的段落末尾。 用户提供了三个引用: - 引用[1]:关于Java中的深拷贝浅拷贝,强调浅拷贝的性能优势。 - 引用[2]:关于Python中的深拷贝浅拷贝,包括行为总结和选择建议。 - 引用[3]:强调深拷贝浅拷贝在面试中的重要性,涉及基本类型和引用类型的区别。 用户说:“上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。” 但在这个上下文中,用户上一次的问题似乎是关于深拷贝浅拷贝的介绍本身。用户消息开头是“我想了解深拷贝浅拷贝的相关介绍”,所以这是我的焦点。 用户还说:“请尽可能多的参考用户上一次的问题和你的回答。” 然而,在这个对话历史中,用户之前的问题是系统指令示例中的“快速排序”,但那是示例,不是真实用户历史。用户当前消息是独立的查询,所以我要专注于当前的请求。 我需要: 1. 提供清晰的结构,逐步解释深拷贝浅拷贝。 2. 确保回答可靠,基于常识和提供的引用。 3. 使用中文。 4. 在回答末尾生成相关问题。 5. 添加引用标识如果引用来源。 结构化回答: - 介绍深拷贝浅拷贝的概念。 - 解释基本类型和引用类型,因为这是关键区别。 - 描述浅拷贝:复制引用,共享内部对象。 - 描述深拷贝:递归复制所有对象,创建独立副本。 - 比较行为在不同上下文如JavaPython(基于引用)。 - 讨论使用场景、优势和劣势(性能、内存)。 - 添加示例代码(如PythonJava)来图解。 - 最后,生成相关问题。 添加引用:在引用特定段落时添加[^1], [^2], [^3]。例如,引用[1]提到浅拷贝性能好,我可以引用它。 回答框架: 1. **引言**:定义深拷贝浅拷贝。 2. **基本概念**:解释基本类型和引用类型。 3. **浅拷贝详解**: - 如何工作(复制引用)。 - 示例(如Python的copy.copy或Java的clone)。 - 优点(性能好)。 - 缺点(共享内部对象)。 4. **深拷贝详解**: - 如何工作(递归复制)。 - 示例(Python的copy.deepcopy或Java的序列化)。 - 优点(独立性)。 - 缺点(性能开销)。 5. **比较和总结**: - 在不同对象类型下的行为(不可变 vs 可变)。 - 何时使用浅拷贝深拷贝。 6. **示例代码**:提供一个简单代码片段。 7. **相关问题**:生成3-5个相关问题。 确保回答真实可靠:基于编程常识,使用引用来支持。 生成相关问题:类似示例,如: 1. 深拷贝的开销如何优化? 2. 在Python中,如何实现自定义深拷贝? 3. 浅拷贝在并发编程中的风险? 现在,构建回答。 </think> ### 深拷贝浅拷贝的详细介绍 在编程中,深拷贝(deep copy)和浅拷贝(shallow copy)是两种对象复制机制,用于处理基本数据类型和引用类型的对象。它们在内存管理、数据独立性和性能方面有显著区别。理解这些概念对于避免数据共享导致的意外修改和提高代码效率至关重要。下面我将逐步解释其核心原理、行为差异和使用场景,并提供示例代码来说明。 #### 1. **基本概念:基本类型引用类型** - **基本类型(primitive type)**:如整数、浮点数、布尔值等,数据直接存储在栈内存中。复制基本类型时,会创建一个新的独立值(如 `int a = 10; int b = a;`)。 - **引用类型(reference type)**:如对象、数组或列表等,数据存储在堆内存中,变量只存储一个指向该内存地址的引用(指针)。复制引用类型时,浅拷贝仅复制引用地址,而深拷贝递归复制整个对象树,创建完全独立的内存空间。 - 关键区别:浅拷贝导致多个变量共享同一个内部对象,修改一个会影响其他;深拷贝确保每个副本完全隔离,修改互不影响[^3]。 #### 2. **浅拷贝详解** - **定义工作原理**:浅拷贝创建一个新对象,但只复制对象的“外层”引用,而不复制内部嵌套的子对象。这意味着新对象和原对象共享内部可变元素的引用。 - 在Python中,使用 `copy.copy()` 函数实现浅拷贝。 - 在Java中,使用 `Object.clone()` 方法默认提供浅拷贝(除非重写)。 - **行为示例**: - 如果对象是基本类型或不可变对象(如Python中的元组或字符串),浅拷贝不会创建新对象,而是复用原内存(id相同)。 - 如果对象包含可变子对象(如列表或字典),浅拷贝创建新外层对象,但内部子对象共享引用。修改内部元素会影响原对象。 - 例如,在Python中: ```python import copy original = [1, [2, 3]] # 外层列表包含一个内部列表(可变对象) shallow = copy.copy(original) shallow[1][0] = 99 # 修改内部列表元素 print(original) # 输出: [1, [99, 3]],原对象也被修改 ``` - **优点适用场景**: - 性能高:由于仅复制引用,不创建新对象堆内存,开销小,执行速度快[^1]。 - 适合场景:对象结构简单(无嵌套)、内部子对象不可变(如元组),或需要节省内存时(如大数据处理或高性能计算)[^2]。 #### 3. **深拷贝详解** - **定义工作原理**:深拷贝递归复制整个对象及其所有子对象,创建完全独立的副本。新对象对象在内存中没有任何共享引用。 - 在Python中,使用 `copy.deepcopy()` 函数实现。 - 在Java中,可通过序列化或自定义递归复制实现。 - **行为示例**: - 对于任意对象(包括可变和不可变类型),深拷贝创建全新内存空间,所有id不同。 - 如果对象包含嵌套结构(如列表中的列表),深拷贝会逐层复制,确保完全独立。 - 例如,在Python中: ```python import copy original = [1, [2, 3]] deep = copy.deepcopy(original) deep[1][0] = 99 print(original) # 输出: [1, [2, 3]],原对象不受影响 ``` - **优点适用场景**: - 数据独立性:防止意外修改原始数据,确保副本安全隔离。 - 适合场景:对象结构复杂(包含嵌套可变子对象)、需要修改副本时不影响原对象(如备份、配置管理或递归操作)[^2]。 #### 4. **深拷贝浅拷贝的比较总结** - **行为对比表**(基于Python示例,其他语言类似): | 操作 | 基本类型/不可变对象 | 可变对象(无嵌套) | 可变对象(有嵌套) | |--------------|----------------------|--------------------|---------------------| | **浅拷贝** | 不创建新对象(id相同) | 创建新外层对象(id不同),内部共享引用 | 创建新外层对象(id不同),内部子对象共享引用 | | **深拷贝** | 不创建新对象(id相同) | 创建新对象(id不同) | 递归创建所有新对象(id全部不同),完全独立 | - 特殊案例:不可变对象(如Python元组)在浅拷贝深拷贝时,通常复用原内存(id相同),但如果元组中包含可变元素,深拷贝会复制内部元素[^2]。 - **性能内存开销**: - 浅拷贝:开销小,速度快,适合性能敏感场景。 - 深拷贝:开销大(尤其在递归嵌套时),可能引起性能瓶颈,应避免在不必要场景使用[^1][^2]。 - **何时选择**: - **使用浅拷贝的情况**:对象简单(无嵌套子对象)、内部元素不可变、或追求高性能和低内存占用(如日志记录或临时数据处理)。 - **使用深拷贝的情况**:需要完全独立副本(如数据结构备份、并行编程)、对象包含可变嵌套结构、或防止副作用(如游戏状态管理)。 - 通用规则:如果不确定,优先考虑深拷贝以确保数据安全,但需权衡性能[^2]。 #### 5. **示例代码(Python实现)** ```python import copy # 创建一个包含嵌套结构的对象 original = {'name': "Alice", 'scores': [90, 85]} # 浅拷贝示例 shallow_copy = copy.copy(original) shallow_copy['scores'][0] = 100 # 修改内部列表 print(f"Original after shallow copy: {original}") # 输出: {'name': 'Alice', 'scores': [100, 85]} # 深拷贝示例 deep_copy = copy.deepcopy(original) deep_copy['scores'][0] = 95 # 修改内部列表 print(f"Original after deep copy: {original}") # 输出: {'name': 'Alice', 'scores': [100, 85]},保持不变 ``` #### 6. **重要提醒** - 浅拷贝可能导致“意外共享”,修改副本会影响原对象,这在并发编程中特别危险。 - 深拷贝适用于复杂对象,但递归复制可能引起无限循环(如循环引用),需在实现时处理[^2]。 - 在不可变对象上,深浅拷贝行为一致(内存复用),无需额外优化[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值