深拷贝与浅拷贝的区别,实现深拷贝的几种方法


什么是浅拷贝?

浅拷贝:复制一层对象的属性,并不包括对象里面的为引用类型的数据,当改变拷贝的对象里面的引用类型时,源对象也会改变。

什么是深拷贝?

深拷贝:重新开辟一个内存空间,需要递归拷贝对象里的引用,直到子属性都为基本类型。两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

如何区分深拷贝与浅拷贝?简单来说,假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝。

PS:深拷贝和浅拷贝一般只针对像 Object, Array 这样的复杂对象的。

所以简单来说,浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。

文中会简单阐述js的基本数据类型与引用数据类型,为了能更好的理解深拷贝与浅拷贝,我们引入了栈堆概念。

javaScript的变量类型

1.ECMAScript 中的变量类型分为两类:

  • 基本类型:undefined,null,布尔值(Boolean),字符串(String),数值(Number),变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。
  • 引用类型: 统称为Object类型,细分的话,有:Object类型,Array类型,Date类型,Function类型等。存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

JavaScript存储对象都是存地址的,所以浅拷贝会导致 arr1 和 arr2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。

2、不同类型的存储方式:

基本数据类型 名值 存储在 栈内存,形式如下:栈内存中分别存储着变量的标识符以及变量的值。

var a  = "A";

在栈内存中是这样的

引用类型 存储在栈内存中, 存储在堆内存中 , 但是栈内存会提供一个引用的地址指向堆内存中的值,当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据。

var a = {name:“jack”};

在内存中是这样的

3、不同类型的复制方式:

基本类型 的复制:当你在复制基本类型的时候,栈内存会新开辟一个内存,相当于把值也一并复制给了新的变量。

栗子 1:

var a = 1;
var b = a;
console.log(a === b);
var a = 2;
console.log(a);
console.log(b);

内存中是这样的:

var a = 1;

var b = a;

a = 2;

改变 a 变量的值,并不会影响 b 的值。因为此时的b已自食其力,翅膀硬了,不受a的影响了。当然,let a=1,b=a;虽然b不受a影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。

引用类型 的复制:当你在复制引用类型的时候,实际上只是复制了指向堆内存的引用地址,并非堆里面的值。即原来的变量与复制的新变量指向了同一个堆里的值。

栗子 2:

var a = {name:"jack",age:20};
var b = a;
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);

改变 a 变量的值,会影响 b 的值。

内存中是这样的:

var a = {name:"jack",age:20};

var b = a;

a.age = 30;

而当我们a.age=30时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。

深拷贝 & 浅拷贝

由此可以明白,所谓浅拷贝:

对于仅仅是复制了引用(地址),换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝

而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝

深浅拷贝 的主要区别就是:复制的是引用(地址)还是复制的是实例。

所以上面的 栗子2 ,如何可以变成深拷贝呢?

我们可以想象出让 b 在内存中像下图这样,肯定就是深拷贝了。

那么代码上如何实现呢?

利用 递归 来实现深复制,对属性中所有引用类型的值,遍历到是基本类型的值为止。

function deepClone(source){    
    if(!source && typeof source !== 'object'){
        throw new Error('error arguments', 'shallowClone');    
    }    
    var targetObj = Array.isArray(source) ? [] : {};    
    for(var keys in source){       
        if(Object.prototype.hasOwnProperty.call(source,keys)){  
            if(source[keys] && typeof source[keys] === 'object'){  
                targetObj[keys] = deepClone(source[keys]);    //递归      
            }else{            
                targetObj[keys] = source[keys];         
            }       
        }    
    }    
    return targetObj; 
}

function deepClone(data){
    if(!data || !(data instanceof Object) || (typeof data=="function")){
        return data||undefined;
    }
    var constructor = data.constructor;
    var result = new constructor();
    for(var key in data){
        if(Object.prototype.hasOwnProperty.call(data,key)){
            result[key]=deepClone(data[key]);
        }
    }
    return result;
}

检测一下

var a = {name:"jack",age:20};
var b = deepClone(a);
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);

最后让我们来看看 一些 js 中的 复制方法,他们到底是深拷贝还是浅拷贝?

1、 Array 的 slice 和 concat 方法

两者都会返回一个新的数组实例。

栗子:

slice:

var a = [1,2,3];
var b = a.slice(); //slice
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);

concat:

var a = [1,2,3];
var b = a.concat();  //concat
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);

看到结果,如果你觉得,这两个方法是深复制,那就恭喜你跳进了坑里

让咱们再看一个颠覆你观念的栗子:

var a = [[1,2,3],4,5];
var b = a.slice();
console.log(a === b);
a[0][0] = 6;
console.log(a);
console.log(b);

嗯?都变啦?

这就是坑,知道吗?

所以 Arraysliceconcat 方法 并不是 真正的深拷贝,他们其实是浅拷贝

2、 jQuery中的 extend 复制方法

可以用来扩展对象,这个方法可以传入一个参数:deep(true or false),表示是否执行深复制(如果是深复制则会执行递归复制)。

栗子:

深拷贝:

var obj = {name:'xixi',age:20,company : { name : '腾讯', address : '深圳'} };
var obj_extend = $.extend(true,{}, obj); //extend方法,第一个参数为true,为深拷贝,为false,或者没有为浅拷贝。
console.log(obj === obj_extend);
obj.company.name = "ali";
obj.name = "hei";
console.log(obj);
console.log(obj_extend);

浅拷贝:

var obj = {name:"xixi",age:20};
var obj_extend = $.extend(false,{}, obj); //extend方法,第一个参数为true,为深拷贝,为false,或者没有为浅拷贝。
console.log(obj === obj_extend);
obj.name = "heihei";
console.log(obj);
console.log(obj_extend);

咦,company 的变化 可以看出 深浅复制来(即箭头所指), 红色方框圈出的地方,怎么和上面 sliceconcat 的情况一样?难道也是羊?

3、JSON 对象的 parse 和 stringify

JOSN 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 js 对象,这两个方法实现的是深拷贝。

栗子:

var obj = {name:'xixi',age:20,company : { name : '腾讯', address : '深圳'} };
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json);
obj.company.name = "ali";
obj.name = "hei";
console.log(obj);
console.log(obj_json);

jQuery.extend( [deep ], target, object1 [, objectN ] )

deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝

target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。

object1 objectN可选。 Object类型 第一个以及第N个被合并的对象。

let a=[0,1,[2,3],4],
  b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);

可以看到,效果与上面方法一样,只是需要依赖JQ库。

4.Object.assign()

基本用法:**Object.assign()** 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象

注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果只有一个参数,Object.assign会直接返回该参数。

Object.assign()方法既不属于深拷贝,也不属于浅拷贝。对于一级的时候,是深拷贝,如果是二级以后对象就是浅拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

5.总结一下:

Arraysliceconcat 方法 和 jQuery 中的 extend 复制方法,他们都会复制第一层的值,对于 第一层 的值都是 深拷贝,而到 第二层 的时候 Arraysliceconcat 方法就是 复制引用jQuery 中的 extend 复制方法 则 取决于 你的 第一个参数, 也就是是否进行递归复制。所谓第一层 就是 key 所对应的 value 值是基本数据类型,也就像上面栗子中的name、age,而对于 value 值是引用类型 则为第二层,也就像上面栗子中的 company 。


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录