0

0

深拷贝与浅拷贝的区别是什么?如何实现深拷贝?

夜晨

夜晨

发布时间:2025-09-03 13:12:02

|

909人浏览过

|

来源于php中文网

原创

深拷贝会递归复制对象所有嵌套属性,确保新旧对象完全独立,而浅拷贝仅复制引用,导致修改相互影响;常用深拷贝方法包括json.parse(json.stringify(obj))、递归函数处理循环引用和特殊对象,或使用lodash的_.clonedeep()及现代api structuredclone()。

深拷贝与浅拷贝的区别是什么?如何实现深拷贝?

浅拷贝仅仅是复制了对象或数组的引用,这意味着新旧变量指向的是内存中的同一块数据,当其中一个修改了内部的复杂类型(比如另一个对象或数组)时,另一个也会随之改变。而深拷贝则不然,它会递归地复制所有嵌套的属性,确保新旧对象在内存中拥有完全独立的存储空间,彼此互不影响。实现深拷贝,通常需要根据对象的结构,逐层、逐属性地进行复制,尤其要关注那些引用类型的数据。

解决方案

理解深拷贝与浅拷贝的核心在于它们对“引用”的处理方式。浅拷贝在遇到对象内部的引用类型属性时,只会复制这个引用本身,而不是引用所指向的实际数据。这就好比你复制了一个文件的快捷方式,而不是文件本身。你通过快捷方式修改了文件内容,原文件自然也变了。深拷贝则不同,它会深入到每一个引用类型属性的内部,将其指向的数据也完整地复制一份,直到所有数据都是原始类型(如数字、字符串、布尔值)为止。这就像你把整个文件都复制了一遍,新旧文件完全独立。

要实现深拷贝,在JavaScript中,最简单粗暴但也最常用的方法是利用

JSON.parse(JSON.stringify(obj))
。这种方法利用了JSON序列化和反序列化的过程,将对象转换为字符串再解析回来,从而切断了所有引用。但需要注意的是,这种方法有其局限性,比如无法处理函数、
undefined
Date
对象(会被转换为字符串)、
RegExp
对象,以及最关键的——循环引用。对于更复杂的情况,我们通常需要编写递归函数,或者借助成熟的第三方库。

浅拷贝的陷阱与潜在风险:为什么理解其行为至关重要?

说实话,我个人在开发中就遇到过好几次因为没搞清楚浅拷贝而引发的“血案”。最典型的场景就是当你从一个配置对象中取出一个子对象,然后直接修改它,结果发现原始的配置对象也跟着变了,这在调试时简直是灾难。

想象一下,你有一个用户设置对象,里面包含一个

theme
子对象,存储着颜色偏好等。如果你只是简单地通过
Object.assign()
或者展开运算符
{...userSettings}
来复制
userSettings
,那么
theme
这个属性其实只是复制了引用。当你尝试修改
copiedSettings.theme.primaryColor = 'blue'
时,原
userSettings.theme.primaryColor
也变成了
blue
。这种隐式修改往往发生在不经意间,尤其是在组件之间传递props,或者在状态管理中处理不可变数据时,如果使用了浅拷贝,就很容易导致数据污染,或者触发不必要的重新渲染。

这种行为模式,尤其在处理数组嵌套对象时,更让人头疼。比如一个包含多个商品对象的购物车数组,如果你浅拷贝了这个数组,然后修改了其中一个商品的某个属性,那么原始购物车数组里的那个商品也会被修改。这不仅影响了数据的纯洁性,也让程序的行为变得难以预测和维护。理解浅拷贝的本质,是避免这些潜在风险的第一步。

实现深拷贝的策略与考量:从简单到复杂

实现深拷贝,方法多种多样,选择哪种取决于你的具体需求和要处理的数据复杂性。

最简单直接的,正如前面提到的,是

JSON.parse(JSON.stringify(obj))
。它的优点是代码简洁,易于理解,对于只包含数字、字符串、布尔值、null以及普通对象和数组的纯数据对象,它工作得很好。但缺点也同样明显,它会丢失函数、
undefined
Symbol
BigInt
等类型,并且会将
Date
对象转换为ISO格式的字符串,
RegExp
对象会变成空对象。更重要的是,它无法处理循环引用,一旦对象内部存在相互引用的情况,它会直接抛出错误。我个人在处理一些简单的API响应数据时,会图方便用它,但只要数据结构稍微复杂一点,就得另寻他法了。

FormX
FormX

AI自动从表格和文档中提取数据

下载

JSON
方法不够用时,我们通常会考虑手写一个递归函数。这需要我们遍历对象的每一个属性,判断其类型。如果是原始类型,直接赋值;如果是对象或数组,则递归调用自身。这里有一个基本的JavaScript实现思路:

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  let cloneObj;
  // 处理日期对象
  if (obj instanceof Date) {
    cloneObj = new Date(obj);
  } 
  // 处理正则表达式
  else if (obj instanceof RegExp) {
    cloneObj = new RegExp(obj);
  }
  // 处理Map、Set等其他特殊对象
  // else if (obj instanceof Map) { ... }
  // else if (obj instanceof Set) { ... }
  // ...
  else {
    cloneObj = Array.isArray(obj) ? [] : {};
  }

  hash.set(obj, cloneObj); // 存储已克隆的对象,防止循环引用

  for (let key in obj) {
    // 确保只复制对象自身的属性,不包括原型链上的
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

这个递归函数需要考虑很多细节,比如如何处理

Date
RegExp
Map
Set
等内置对象,以及如何优雅地处理原型链上的属性。最重要的是,它需要一个机制来处理循环引用,否则会陷入无限循环。上面代码中的
WeakMap
就是用来记录已经访问过的对象,避免重复克隆和处理循环引用的。

对于更健壮和全面的深拷贝,很多时候我们会选择使用成熟的第三方库,比如Lodash的

_.cloneDeep()
。这些库经过了大量的测试和优化,能够处理各种复杂的边缘情况,包括循环引用、特殊对象类型、不可枚举属性等,用起来省心省力。在生产环境中,如果不是对性能有极致要求且能精准控制数据结构,我个人更倾向于使用这样的库。

优化深拷贝性能与应对特殊场景:循环引用与特殊对象

深拷贝并非没有代价,尤其是在处理大型或深度嵌套的对象时,其性能开销是需要考虑的。

JSON.parse(JSON.stringify(obj))
虽然代码简洁,但其内部的序列化和反序列化过程本身就是耗时操作,对于超大对象,性能瓶颈会非常明显。手写递归函数虽然灵活,但如果实现不当,也可能效率低下。

循环引用是一个深拷贝的经典难题。当一个对象A引用了对象B,而对象B又引用了对象A时,就形成了循环引用。如果深拷贝函数没有处理机制,它会无限递归下去,最终导致栈溢出。前面提到的

WeakMap
Map
就是解决这个问题的有效方法:在克隆一个对象之前,先检查它是否已经被克隆过。如果已经克隆过,就直接返回之前克隆的副本,而不是再次递归。这就像给每个已经处理过的对象打上一个“已访问”的标记。

特殊对象的处理也是深拷贝的另一个难点。除了前面提到的

Date
RegExp
,还有
Map
Set
Promise
Error
对象,甚至自定义类的实例。一个通用的深拷贝函数需要针对这些类型进行专门的判断和处理。例如,对于
Map
Set
,需要遍历它们的成员并递归克隆;对于函数,通常是直接复制引用(因为函数是行为,而非数据,深拷贝函数本身意义不大);对于自定义类的实例,可能需要调用其构造函数并逐一复制属性,甚至考虑原型链上的属性。

值得一提的是,现代浏览器和Node.js环境中,已经有了

structuredClone()
这个API,它实现了结构化克隆算法,能够非常高效且正确地处理大部分复杂类型,包括循环引用、
Date
RegExp
Map
Set
ArrayBuffer
等。如果你的目标环境支持这个API,那无疑是实现深拷贝的最佳选择,它比手写递归函数更健壮,比
JSON
方法更全面,性能也通常更好。当然,对于一些老旧环境的兼容性考虑,手动实现或引入第三方库依然是必要的。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

452

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

546

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

329

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

81

2025.09.10

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

252

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

988

2024.03.01

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1561

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

241

2024.02.23

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

4

2026.03.04

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 5.7万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.3万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号