有关实现深拷贝的四种方法

news2024/11/19 5:48:46

        深拷贝与浅拷贝:

        在开始之前我们需要先了解一下什么是浅拷贝和深拷贝,其实深拷贝和浅拷贝都是针对的引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型赋值,则会进行地址的拷贝,最终两个变量指向同一份数据。

// 基本类型
var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 ,a b指向不同的数据

// 引用类型指向同一份数据
var a = {c: 1};
var b = a;
a.c = 2;
console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据

        对于引用类型,会导致a b指向同一份数据,此时如果对其中一个进行修改,就会影响到另外一个,有时候这可能不是我们想要的结果,如果对这种现象不清楚的话,还可能造成不必要的bug.

        那么如何切断a和b之间的关系呢,可以拷贝一份a的数据,根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝。

    var a1 = {b: {c: {}}};

    var a2 = shallowClone(a1); // 浅拷贝
    a2.b.c === a1.b.c // true
 var a3 = clone(a1); // 深拷贝
 a3.b.c === a1.b.c // false

这里认为shallowClone是指浅拷贝,clone是指深拷贝,a1对象是一个三层嵌套对象。浅拷贝只能拷贝第一层,b属性是一个对象,b里面存放的是这个对象的引用地址,因此a1.b.c===a2.b.c ; 深拷贝则是拷贝每一层,a1.b和a3.b的引用地址不同,完全独立,因此a3.b.c!=a1.b.c.

对于浅拷贝的实现来说非常简单,我们对要拷贝的对象或数组进行一次遍历,并赋值给创建的新对象或新数组,就可完成浅拷贝,这里不再多说。 下面我们将讨论深拷贝应该如何实现。这里介绍四种深拷贝方法。

1.递归法:

        实现深拷贝最常用的方法便是递归法,原理就是通过递归的方式遍历要拷贝的对象或数组。首先对要拷贝的对象或数组进行类型判断,然后创建相应的一个空对象或空数组,然后和浅拷贝一样,通过循环遍历这个对象或数组,若发现某个属性是引用数据类型的话,就递归调用深拷贝函数,若该属性是基本数据类型,则直接进行赋值操作,直至遍历完所有子对象的所有属性。递归遍历本质上是深度优先遍历,在进行深拷贝时,若一个属性的值为对象,则优先对这个子对象进行拷贝,而不是拷贝与这个属性相邻的下一个属性。待这个属性所对应的子对象整体拷贝完毕后,才会拷贝下一个属性。下面放上递归法的代码:

        

 //递归遍历每个属性进行深拷贝
    function clone(source) {
        let target = Array.isArray(source) ? [] : {};
        if(typeof(source)!="object") {
            target = source;
            return target;
        }
        if(source instanceof Array) {
            for(var i=0;i<source.length;i++) {
                if(typeof source[i] == "object") {
                    target[i] = clone(source[i]);
                }else {
                    target[i] = source[i];
                }
            }
        }
        else {
            for(let key in source) {
                if(source.hasOwnProperty(key)) {
                    if(typeof source[key] == "object") {
                        target[key] = clone(source[key]);
                    }else {
                        target[key] = source[key];
                    }
                }
            }
        }
        return target;
    }

在这里用一个包含多层的对象数组进行测试,并将深拷贝的属性进行修改,看看是否会影响到原始数据。

   let e = [{a:'1',b:'2',c:'3'},{aa:'11',bb:'22',cc:'33'},{aaa:'111',bbb:'222',ccc:'333'}];
    let f = clone(e);
    f[2].ccc='444';
    console.log(e);
    console.log(f);

 

可以看到打印后的结果,修改拷贝数组第三个元素的ccc属性后,原数组并没有发生变化,说明深拷贝实现成功。

  1. JSON深拷贝法:

利用系统自带的JSON也可以实现深拷贝。

JSON.parse(JSON.stringify(a));

实际上是将要拷贝的对象先转换为JSON字符串,再将字符串转换为对象。JSON对象与字符串转换的原理实际上也是做递归遍历,只不过需要做两次,因此在算法效率上递归法要比JSON转换高一倍。

但使用递归法和JSON进行深拷贝有一个弊端,就是在每次执行递归函数时,都会调用内存空间,所以当要拷贝的对象深度非常大时,使用递归函数会出现爆栈的问题。下面来模拟下这种情况。

 //构造指定深度和广度的对象
    function createData(deep,num) {
        var data = {};
        var temp = data;
        for(var i=0;i<deep;i++) {
            temp.data = {};
            temp = temp.data;
            for(var j = 0;j<num;j++) {
                temp[j] = j;
            }
        }
        return data;
        }

这个createData函数用来构造一个深度为deep,广度为num的对象。例如createData(3,3),创建出的对象为{data:{‘0’:0,’1’:1,’2’:2,data:{‘0’:0,’1’:1,’2’:2,data:{‘0’:0,’1’:1,’2’:2,}}}}。此时用createData创建一个深度为10000的对象,并用递归法clone()和JSON拷贝这个对象,会发现内存溢出报错。

 let d = createData(10000,0);
    let e = clone(d);
    console.log(e);

 let d = createData(10000,0);
    let e = JSON.parse(JSON.stringify(d));
    console.log(e);

 

 

可见碰到层级很多的对象时,这两种方法就不管用了,下面就来介绍第三种方法,循环递归深拷贝,这种方法可以很好的解决内存溢出的问题。

2.循环遍历法:

如果我们可以使用循环来遍历对象所有层级的属性,就可以省去递归调用函数时占用的内存空间,从而解决爆栈的问题。那么该如何运用循环的思想实现多层次遍历呢。下面来看一个数据结构

 var a = {
    a1: {
        a11: 1,
    },
    a2: {
        b1: 1,
        b2: {
            c1: 1
        }
    }
    }

a对象是一个嵌套对象,如果我们把它竖着来看,会发现它是一个树形的数据结构。

    a
  /   \
 a1   a2        
 |    / \         
 a11 b1 b2     
 |   |   |        
 1   1  c1
         |
         1 

我们可以把对象中引用数据类型的属性看作是这颗树里面的子节点,基本数据类型的属性看作是叶子节点的父节点。会发现我们只需要遍历这颗树的每个节点,就可以实现深拷贝了。用循环遍历这棵树,我们需要借助一个栈,把子节点(除叶子节点和其父节点外)放入到这个栈中,表示这些节点需要拷贝,每次拷贝从栈中放出一个节点进行拷贝,当栈为空时,则表示这棵树已经遍历完并完成拷贝。循环遍历本质上就是对栈中的元素进行循环,栈空循环自然结束。下面来模拟一下利用栈循环遍历的这个过程。

在循环前我们先构造这个栈,栈是一个具有后进先出特点的数组,我们要将需要遍历的节点放入到栈中,在刚开始拷贝时,因为不知道要拷贝的对象里面的属性如何,所以先往栈里放入一个种子数据。栈中存放的元素需要有三个属性来描述节点信息,parent 表示当前所处的节点,也就是要做循环遍历的当前节点,key用来存放当前节点中属性名为key的子元素,表示需要拷贝的子对象,data表示要拷贝子对象的内容。

 {
                parent: root,
                key: undefined,
                data: x,
            }

拿要拷贝的a对象来举例,我们想把它拷贝到root中,先构造root空对象,然后将上面这个种子数据放入到栈中,遍历前我们处在root节点,所以parent为root,因为不知道拷贝的对象结构如何,所以key为undefined,data表示要拷贝的内容,也就是整个a对象。

第一轮循环开始,种子数据出栈,循环遍历当前parent节点下的节点,发现a1 a2都是引用数据类型,是要拷贝的节点,所以把他们对应的节点信息存入栈中,此时parent为root,key为a1和a2  data分别为 a1 对象和a2对象。因为a2在a1之后存入,则a2先出栈。

第二轮循环:循环遍历a2下的节点,b2为引用类型,b2对应节点信息存入到栈中,因为在a2下进行遍历,所以parent为a2。

第三轮循环:b2出栈,遍历b2下的节点,发现没有引用类型,直接拷贝。

第四轮循环:a1出栈,遍历a1下的节点,发现a11不是引用类型,直接拷贝。此时栈为空,并且没有需要入栈的引用类型,则表示已经遍历完毕,循环结束。

通过这个过程我们可以发现循环遍历的次数取决于要拷贝的数据中引用数据类型的个数,嵌套了多少个对象,就循环多少次。并且由于栈后进先出的特点,循环遍历同递归遍历一样也属于深度优先遍历。即先访问子元素,再访问兄弟元素。

下面贴出循环遍历深拷贝方法的代码

 //循环遍历对象实现深拷贝
    function cloneLoop(x) {
        const root = {};

        // 栈 后进先出
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
        while(loopList.length) {
            // 深度优先
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
            // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
        return root;
    }

这里为了方便,只考虑对象情况,若要考虑数组情况,则需要对拷贝类型进行判断,根据类型构造空对象或空数组。具体实现参考递归法。

通过循环遍历,我们可以破解递归爆栈,利用循环深拷贝法cloneLoop()对刚才的10000层对象进行拷贝,发现没有报栈溢出的错误,说明此方法有效。

 let d = createData(10000,0);
    let e = cloneLoop(d);
    console.log(e);

 

尽管循环遍历法可以解决拷贝层级很多对象时内存溢出的问题,但对于某些特殊情况依然无法应对。如循环引用对象或是属性间存在引用关系的对象。例如 :

        a={};

        a.a=a;

        var obj = {};

        var obj1 = {a1: obj, a2: obj};

a是循环引用对象,obj1是属性间存在引用关系的对象。像这样的对象上述三种方法都无法实现深拷贝后仍然保持引用关系,也无法破解循环引用。我们拿obj1来用这三种方法进行拷贝测试,结果如图:

    var obj = {};
    var obj1 = {a1: obj, a2: obj};

    console.log(obj1.a1 === obj1.a2) // true
    var obj3 = cloneLoop(obj1);
    console.log(obj3.a1 === obj3.a2) //false
    var obj4 = JSON.parse(JSON.stringify(obj1));
    console.log(obj4.a1 ===obj4.a2) //false
    var obj5 = clone(obj1);
    console.log(obj5.a1 === obj5.a2) //false 

 

这里就要介绍第四种方法,它可以解决循环引用和属性间存在引用关系的问题。

3.破解循环引用法:

第四种方法其实是在循环遍历法的基础上进行改进。在每轮循环开始前如果我们发现个新对象就把这个对象和他的拷贝存下来,每次拷贝对象前,都先看一下这个对象是不是已经拷贝过了,如果拷贝过了,就不需要拷贝了,直接用原来的,这样我们就能够保留引用关系了。

引入一个数组uniqueList用来存储已经拷贝过的对象,每次循环遍历时,先判断本次要拷贝的对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了,直接将上次拷贝的对象的引用直接赋值给该要拷贝的属性。如果不在则表示要拷贝对象第一次出现,将这个对象和拷贝后的对象的引用存入uniqueList中。 代码如下:

/ 保持引用关系
    function cloneForce(x) {
        // =============
        const uniqueList = []; // 用来去重
        // =============

        let root = {};
        // 循环数组
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
        while(loopList.length) {
            // 深度优先
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
            // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
            
            // =============
            // 数据已经存在
            let uniqueData = uniqueList.find((item) => item.source === data);
            if (uniqueData) {
                parent[key] = uniqueData.target;
                continue; // 中断本次循环
            }
            // 数据不存在
            // 保存源数据,再拷贝数据中对应的引用
            uniqueList.push({
                source: data,
                target: res,
            });
            // =============
        
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
        return root;
    }

运用改进后的循环遍历法cloneForce()对obj1进行测试,发现深拷贝后的对象里仍然保留了属性间的引用关系,说明该方法对于破解循环引用有效。

    var obj = {};
    var obj1 = {a1: obj, a2: obj};

    console.log(obj1.a1 === obj1.a2) // true
    var obj2 = cloneForce(obj1);
    console.log(obj2.a1 === obj2.a2) // true

 

由于没拷贝过的新对象都要进入uniqueList数组,在每次拷贝前都要遍历uniqueList数组检查要拷贝的对象是否出现,当要拷贝的子对象过多并且只有较少量的属性间存在引用关系时,每次遍历uniqueList数组都会做无意义的判断从而导致算法效率降低。因此该方法只适用于数据量不大并且需要保持属性间的引用关系且符合这种关系的属性数量多。对于数据量较大的对象拷贝在不要求保持引用关系的情况下还是用第三种方法更好。

以上就是关于实现深拷贝的四种方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Logstash学习

一、Logstash基础 1、什么是Logstash logstash是一个数据抽取工具&#xff0c;将数据从一个地方转移到另一个地方。下载地址&#xff1a;https://www.elastic.co/cn/downloads/logstash logstash之所以功能强大和流行&#xff0c;还与其丰富的过滤器插件是分不开的&#xff…

CDGP认证|ChatGPT的出现,对数据治理行业冲击如何?

ChatGPT的出现对数据治理有很多好处&#xff0c;其中最明显的是提供了更高效、更准确和更自动化的数据处理和分析服务,可以帮助企业和组织更好地管理和利用数据资源&#xff0c;提高数据质量和决策效率。此外&#xff0c;ChatGPT还能够发现隐藏在大量数据中的信息和趋势&#x…

OJ练习第82题——填充书架

填充书架 力扣链接&#xff1a;1105. 填充书架 题目描述 给定一个数组 books &#xff0c;其中 books[i] [thicknessi, heighti] 表示第 i 本书的厚度和高度。你也会得到一个整数 shelfWidth 。 按顺序 将这些书摆放到总宽度为 shelfWidth 的书架上。 先选几本书放在书架…

Nexus 组件发布失败、npm 登录失败 解决过程

目录 参考发布文章进行打包 提示发布成功&#xff0c;但在 Nexus 里没发现组件 测试 yarn 发布 测试 npm 发布&#xff08;解决登录失败&#xff09; Nexus 设置 Sonatype Nexus Repository Manager 相关权限 参考发布文章进行打包 整体发布&#xff1a;根目录运行 yarn r…

直播软件app开发:如何保证音视频质量?

随着社交媒体的发展&#xff0c;视频直播已成为越来越流行的社交方式。直播软件app开发也因此成为了一个热门话题。在开发直播软件app时&#xff0c;保证音视频质量是至关重要的。本文将介绍如何确保你的直播软件app在音视频质量方面表现出色。 确定音视频质量标准 首先&…

第七章 建造者模式

文章目录 前言一、传统方式解决盖房子需求完整代码抽象房子类 AbstractHouse实现子类 普通房子实现子类 高楼大厦客户端盖房子 二、引入建造者模式建造者模式的四个角色&#xff1a; 产品、抽象建造者、具体建造者、指挥者完整代码House类 (产品角色)抽象父类&#xff08;抽象建…

CV 领域的 ChatGPT?MetaAI 推出“最强”大视觉模型 SAM

出品人&#xff1a;Towhee 技术团队 随着 ChatGPT 引起一波又一波的“GPT热潮”&#xff0c;自然语言领域又一次成为了人工智能的讨论焦点。大家不由得思考&#xff0c;计算机视觉领域里是否会出现这样一个堪称划时代的模型&#xff1f;在这种万众瞩目的时候&#xff0c;一直处…

Python3《机器学习实战》学习笔记(七):支持向量机原理篇之手撕线性SVM

文章目录 一、SVM介绍二、线性SVM2.1 数学建模2.1.1决策面方程2.1.2"分类间隔"方程2.1.3约束条件2.1.4线性SVM优化问题基本描述2.1.5求解准备(讲讲凸函数)2.1.6拉格朗日函数2.1.7KKT条件2.1.8对偶问题求解2.1.9最后求解 2.2 SMO算法 三、代码实战3.1准备数据 一、SVM…

M_Map工具箱简介及地理图形绘制

M_Map工具箱简介及地理图形绘制 1 M_Map简介1.1 具体代码说明 2 地理图形绘制案例2.1 M_Map给定案例2.1.1 M_Map Logo2.1.2 Lambert Conformal Conic projection of North American Topography2.1.3 Stereographic projection of North Polar regions2.1.4 Colourmaps 2.2 案例…

vue封装公共组件库并发布到npm库详细教程

vue组件封装的原理&#xff1a;利用vue框架提供的api: Vue.use( plugin )&#xff0c;我们需要把封装好组件的项目打包成vue库&#xff0c;并提供install方法&#xff0c;然后发布到npm中。Vue.use( plugin )的时候会自动执行插件中的install方法。 一、组件库代码目录 目录…

常见的四种排名函数的用法(sql)

四个排名函数&#xff1a; 1.row_number 2.rank 3.dense_rank 4.ntile 1. ROW_NUMBER&#xff08;排名场景推荐&#xff09; 1.1 介绍 在 SQL 中&#xff0c;ROW_NUMBER() 是一个窗口函数&#xff0c;它为结果集中的每一行分配一个唯一的序号。该函数的语法如下&#xff1a; …

内网渗透之横向移动PTHPTTPTK

0x00前言 pass the hash&#xff08;哈希传递攻击&#xff0c;简称pth&#xff09; pass the ticket&#xff08;票据传递攻击&#xff0c;简称ptt&#xff09; pass the key&#xff08;密钥传递攻击&#xff0c;简称ptk&#xff09; PTH(pass the hash) #利用的lm或ntlm的值…

【社区图书馆】 Go佬—Go程序开发实战宝典书评

文章目录 前言内容介绍文章大致划分总结 前言 《Go 程序开发实战宝典》是一本非常实用的 Go 语言开发工具书&#xff0c;本书由深入浅出的案例讲解、详细的技术实现、贴近实际的应用开发等组成&#xff0c;非常适合 Go 语言开发爱好者、从事相关行业的工程师、技术负责人以及深…

ThinkPHP视图

ThinkPHP视图 前言视图一、运算符二、模版函数三、循环标签四,volist 循环标签五&#xff0c;if 判断标签六&#xff0c;switch 判断标签七、包含文件八、其他标签1. 条件标签2. 比较标签3. 循环标签4. 杂项标签 总结 前言 ThinkPHP视图基本语法和PHP语法非常的像&#xff0c;所…

React Native 9个好用的开发工具盘点

近几年在大前端的开发领域&#xff0c;选择跨端方案的公司和部门越来越多&#xff0c;曾一何时市面有不下10种跨端框架&#xff0c;但随着“生物进化论”的推动&#xff0c;目前市面上仅剩两种主流方案&#xff0c;就是经常听到的 React Native 和 Flutter。去年终于引来了 Rea…

企业网络安全漏洞分析及其解决_kaic

摘要 为了防范网络安全事故的发生,互联网的每个计算机用户、特别是企业网络用户&#xff0c;必须采取足够的安全防护措施&#xff0c;甚至可以说在利益均衡的情况下不惜一切代价。事实上&#xff0c;许多互联网用户、网管及企业老总都知道网络安全的要性&#xff0c;却不知道网…

《JavaEE初阶》多线程进阶

《JavaEE初阶》多线程进阶 文章目录 《JavaEE初阶》多线程进阶常见锁策略乐观锁与悲观锁普通互斥锁与读写锁轻量级锁与重量级锁自旋锁与挂起等待锁公平锁和非公平锁可重入锁和不可重入锁 CAS什么是CASCAS的应用场景:CAS 的 ABA 问题什么是CAS 的 ABA 问题解决方案: synchronize…

libxml2交叉编译和移植

编译后的libxml2如果交叉编译环境一致可以直接使用资源-CSDN文库 嵌入式开发过程中实用libxml2进行xml文件的解析、创建、使用。 这里介绍一下该libxml2的交叉编译过程和移植步骤 下载地址 libxml2 各版本下载地址 ftp://xmlsoft.org/libxml2/ ftp://xmlsoft.org/libxml2…

【汽车电子】5分钟了解汽车操作系统(科普篇)

在智能汽车电动汽车的浪潮下&#xff0c;「软件定义汽车」的理念已经成为很多厂家的共识&#xff0c;未来决定汽车个性化差异的不再是马力大小、座椅材质、底盘软硬等&#xff0c;而应该是人工智能、大数据和云计算技术的综合体。 要想实现这一切&#xff0c;就要给汽车安装一个…

【论文简述】WT-MVSNet: Window-based Transformers forMulti-view Stereo(arxiv 2023)

一、论文简述 1. 第一作者&#xff1a;Jinli Liao、Yikang Ding 2. 发表年份&#xff1a;2023 3. 发表期刊&#xff1a;arxiv 4. 关键词&#xff1a;MVS、3D重建、Transformer、极线、几何约束 5. 探索动机&#xff1a;然而&#xff0c;在没有极几何约束的情况下匹配参考图…