02-浅拷贝和深拷贝

Posted Full-Stack-python

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了02-浅拷贝和深拷贝相关的知识,希望对你有一定的参考价值。

前言

在 JavaScript 的编程中经常需要对数据进行复制,这就涉及到浅拷贝和深拷贝,是非常重要的概念。

浅拷贝

概念

创建一个新的对象B,来接收你要重新复制的对象A的值:

  • 如果对象A里面的属性是基本类型,拷贝的是基本类型的值;
  • 但如果对象A里面的属性是引用类型,拷贝的是内存中的地址(不是拷贝)。也就是说,拷贝后的内容和原始内容,指向的是同一个地址。如果一个对象的属性值发生了变化,另一个对象的属性值也会发生变化。

浅拷贝在拷贝引用类型的数据时,只拷贝第一层的属性,再深层的属性无法进行拷贝。用一个成语形容叫“藕断丝连”。

深拷贝

概念

创建一个新的对象B,来接收你要重新复制的对象A的值:

  • 在堆内存中开辟了一块全新的内存地址,将对象A的属性完全复制过来。
  • 这两个对象相互独立、互不影响,彻底实现了内存上的分离。

下面讲一下实现深拷贝的几种方式。

方式1:JSON.stringify() 和 JSON.parse()

这是最简单的深拷贝方法,先把对象序列化成 json 字符串,然后将JSON 字符串生成一个新的对象。

代码实现:

let obj1 =  a:1, b:[1,2,3] 
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2);   //a:1,b:[1,2,3]

obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //a:2,b:[1,2,3,4]
console.log(obj2);   //a:1,b:[1,2,3]

方式1属于乞丐版。缺点是:

(1)主要缺点:

  • 无法拷贝函数、undefined、symbol。经过 JSON.stringify 序列化之后的字符串中这个键值对会消失。
  • 无法拷贝 Map、Set;
  • 无法拷贝对象的循环引用,即 obj[key] = obj。

(2)其他缺点:

  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

无法拷贝函数的代码举例:

const obj =  fn: () => , name: \'qianguyihao\' ;
console.log(JSON.stringify(obj)); // "name":"qianguyihao"

无法拷贝循环引用的代码举例:

const obj =  fn: () => , name: \'qianguyihao\' ;
obj.self = obj;
/*
	控制台报错:
		Uncaught TypeError: Converting circular structure to JSON
		--> starting at object with constructor \'Object\'
		--- property \'self\' closes the circle
		at JSON.stringify (<anonymous>)
*/
console.log(JSON.stringify(obj));

小结:如果你的数据结构是简单的数据类型,使用方式1是最简单和快捷的选择;但如果数据类型稍微复杂一点,方式1 就不行了。

方式2:手写递归

如果只考虑简单的数组、对象,方式2是满足要求的。

const obj1 = 
    name: \'qianguyihao\',
    age: 30,
    address: 
        city: \'shenzhen\'
    


const obj2 = deepClone(obj1)
obj2.address.city = \'beijing\'
console.log(obj1.address.city)

/**
 * 深拷贝
 * @param Object obj 要拷贝的对象
 */
function deepClone(obj = ) 
    // 1、判断是值类型还是引用类型
    if (typeof obj !== \'object\' || obj == null) 
        // obj 如果不是对象和数组,或者是 null,就直接return
        return obj
    

    // 2、判断是数组还是对象
    // 初始化返回结果:数组或者对象
    let result
    if (obj instanceof Array) 
        result = []
     else 
        result = 
    

    for (let key in obj) 
        // 保证 key 不是原型的属性
        if (obj.hasOwnProperty(key)) 
            // 3、递归【关键代码】
            result[key] = deepClone(obj[key])
        
    

    return result


let obj1 = 
  a:
    b:1
  

let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  a:b:1

上面的代码,还有一种写法,更容易理解:


function deepClone(obj) 
    let cloneObj = 
    for(let key in obj)                  // 遍历
        if(typeof obj[key] ===\'object\') 
        cloneObj[key] = deepClone(obj[key])  // 是对象就再次调用该函数递归
         else 
        cloneObj[key] = obj[key]  // 如果是基本类型,直接复制值
        
    
    return cloneObj


let obj1 = 
    a:
        b:1
    

let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  a:b:1

方式2只考虑了 object 和 Array这种 对普通的引用类型的值,是属于比较基础的深拷贝。缺点是:

(1)主要缺点:

  • 无法拷贝函数 Function。
  • 无法拷贝 Map、Set。
  • 无法拷贝对象的循环引用,即 obj[key] = obj。

(2)其他缺点:

  • 无法拷贝不可枚举的属性以及 Symbol 类型。
  • 无法拷贝 Date、RegExp、Error 这样的引用类型。

方式3:改进版

针对上面几个问题,可以用如下几点改进:

(1)针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;

(2)当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;

(3)利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;

(4)利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。

/**
 * 深拷贝
 * @param obj obj
 * @param map weakmap 为了避免循环引用
 */
function cloneDeep(obj, map = new WeakMap()) 
    if (typeof obj !== \'object\' || obj == null ) return obj

    // 避免循环引用
    const objFromMap = map.get(obj)
    if (objFromMap) return objFromMap

    let target = 
    map.set(obj, target)

    // Map
    if (obj instanceof Map) 
        target = new Map()
        obj.forEach((v, k) => 
            const v1 = cloneDeep(v, map)
            const k1 = cloneDeep(k, map)
            target.set(k1, v1)
        )
    

    // Set
    if (obj instanceof Set) 
        target = new Set()
        obj.forEach(v => 
            const v1 = cloneDeep(v, map)
            target.add(v1)
        )
    

    // Array
    if (obj instanceof Array) 
        target = obj.map(item => cloneDeep(item, map))
    

    // Object
    for (const key in obj) 
        const val = obj[key]
        const val1 = cloneDeep(val, map)
        target[key] = val1
    

    return target

C#的浅拷贝和深拷贝

在C++语法中我记得是这样规定的,浅拷贝只拷贝指向元素的引用,等同于只复制一份地址。该其一必定变其二,在C#语法中clone应该就是浅拷贝的,问题来了,..
class Program

static void Main(string[] args)

string[] str = new string[] "ds", "ewr", "324", "sd45" ;
string[] bs = (string[])str.Clone();
for (int i = 0; i < bs.Length; i++)

Console.WriteLine(bs[i]);

Console.WriteLine("浅拷贝后");
str[1] = "你好";
for (int i = 0; i < bs.Length; i++)

Console.WriteLine(bs[i]);

Console.ReadKey();


值根本没有被改变,请问我的问题出在哪里,而把 string[] bs = (string[])str.Clone();改成string[] bs=str;就可以实现,这个我懂,因为数组是引用类型,请问clone()为什么不行

请教下2位: 为什么我定义的类型里面有个Hashtable,这个Hashtable应该是引用类型吧?为什么无法达到浅拷贝的效果啊?

//add 20140520
//我明白了,因为v1.parts = ht1完全改变了v1中parts的引用,此时跟v5的parts已经完全断开了关系.
//只有不改变引用时,浅拷贝才可以相互影响


代码如下:

类型: 

[Serializable]
    public class Vehicle : ICloneable
    

        public Hashtable _parts = new Hashtable();
        public Hashtable parts  get  return _parts;  set  _parts = value;  


        public static Vehicle operator +(Vehicle v1, Vehicle v2)
        
            Hashtable ht = new Hashtable();
            foreach (DictionaryEntry item in v1.parts)
            
                ht.Add(item.Key, item.Value);
            
            foreach (DictionaryEntry item in v2.parts)
            
                ht.Add(item.Key, item.Value);
            
            v2.parts = ht;
            return v2;
        

        public object this[string key]
        
            get
            
                return parts[key];
            
            set
            
                parts[key] = value;
                //parts.Add(key, value);

            
        

        public static implicit operator Vehicle(Hashtable ht)
        
            Vehicle v = new Vehicle();
            v.parts = ht;
            return v;
        

        #region ICloneable Members

        public object Clone()
        
            return this.MemberwiseClone();
        

        #endregion

        public Vehicle Shallow()
        
            return Clone() as Vehicle;
        

        public Vehicle DeepClone()
        
            using (Stream objectstream = new MemoryStream())
            
                IFormatter formatter = new BinaryFormatter();
                formatter.Serialize(objectstream, this);
                objectstream.Seek(0, SeekOrigin.Begin);
                return formatter.Deserialize(objectstream) as Vehicle;
            
        

    

调用: 

            Vehicle v1 = new Vehicle()
            Hashtable ht2 = new Hashtable();
            ht2.Add("str1","haha");
            ht2.Add("str2","hehe");
            v1.parts = ht2;
            Vehicle v4 = v1.DeepClone();
            Vehicle v5 = v1.Shallow() as Vehicle;//v5是v1的浅拷贝

            EnumerateHashtable(v1);//此方法是枚举v1中的Hashtable
            Console.WriteLine("-------------");
            EnumerateHashtable(v4);
            Console.WriteLine("-------------");
            EnumerateHashtable(v5);
            Console.WriteLine("=============");
            Hashtable ht1 = new Hashtable();
            v4.parts = ht1;
            v1.parts = ht1;//v1的hashtable重置了,问题: 但v5的没有重置,求解
            EnumerateHashtable(v1);
            Console.WriteLine("-------------");
            EnumerateHashtable(v4);
            Console.WriteLine("-------------");
            EnumerateHashtable(v5);
            Console.WriteLine("=============");
            Console.ReadLine();

结果:

str1: haha
str2: hehe
-------------
str1: haha
str2: hehe
-------------
str1: haha
str2: hehe
=============
-------------
-------------
str1: haha
str2: hehe
=============

参考技术A string是个比较特别的引用类型,以下是转的:
(3)数组的clone()方法所创建的是数组的浅表副本。即只会复制数组的元素,而并不会复制数组所引用的对象。也就是说,只复制对象指针,而不复制对象本身。

例如:

Class1[] arrayCls1=new
Class1[3];
//创建对象数组arrayCls1
arrayCls1[2] = new
Class1(); //为数组arrayCls1第3个元素赋值
Class1[] arrayCls2 =
(clsCase1[])arrayCls1.Clone();//创建对象数组arrayCls2,并把arrayCls1复制于他
arrayCls2[2].clsText =
"CHAMPION"; //修改数组arrayCls2第3个元素的clsText属性

private class Class1

private
string _value = "MILAN";
public
string clsText

set
_value = value;
get
return _value;



最后结果为:arrayCls1[2].clsText=CHAMPION,arrayCls2[2].clsText=CHAMPION

再看一个例子
string[]
arrayStr1 = "MILAN","INTER","MU" ;
string[]
arrayStr2 = (string[])(arrayStr1.Clone());
arrayStr2[2]
= "ROMA";

最后结果为:arrayStr1[2]=MU,arrayStr2[2]=ROMA

为何类Class1和string都是引用类型,输出的结果却不同呢?

因为string是个特殊的引用类型,每当被赋值时,都会创建一个新的string的对象。

所以string数组在clone一个新数组后,对新数组某元素赋值后,由于创建了一个新的string的对象,所以元素中存放的字符串指针也随之改变了。追问

教我怎么看清一个对象是怎么实现的内部过程。好像和CLR什么有关系吧。平时听别人说过什么IL反汇编可以是羡慕不知道是不是这样,还有你的最后一句话我个人的理解应该是利用克隆之后在新的堆里面从新对象,在新的栈里面重新分配值吧~!(还有我个人也感觉这个string[]数组)和别的引用对象还是有很大的区别的,内部做了很多东西

本回答被提问者和网友采纳
参考技术B 简单来说就是拷贝新的一份赋值给了bs,而修改str和bs是没有关系的
string不能单纯的理解为引用类型,其实更具备值类型的特征,因为使用的太频繁所以进行了特殊处理

以上是关于02-浅拷贝和深拷贝的主要内容,如果未能解决你的问题,请参考以下文章

Python中浅拷贝和深拷贝的区别

NumPy之浅拷贝和深拷贝

C#的浅拷贝和深拷贝

JS-[浅拷贝和深拷贝]

c/c++/c++11 浅拷贝和深拷贝

js对象浅拷贝和深拷贝详解