js中的深拷贝和浅拷贝详解

一、浅拷贝与深拷贝应用的数据类型简介

js数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。

基本数据类型的特点: 直接存储在栈(stack)中的数据,分配的内存大小确定。

引用数据类型的特点: 存储的是该对象在栈中引用,真实的数据存放在堆内存里,动态分配内存,内存大小不定,即引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
​​
图片描述
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。 深拷贝和浅拷贝的示意图大致如下:
在这里插入图片描述

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

二、赋值和浅拷贝的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的同一存储空间的内容,因此,两个对象是联动的。

案例一:

//赋值操作
var obj1 = {
	"name":"zhangsan",
    "age":18,
    "language":["Chinese",["English","American"],"French"]
}
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["Korean","Japanese"];
console.log(obj1,obj2);

结果如下图所示:
在这里插入图片描述

赋值操作只是将引用类型对象保存在栈中的地址赋给另一个新创建的对象,故obj1和obj2共享栈中地址所指向的堆中的内存。

浅拷贝 是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址中保存的内容,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

案例二:

//浅拷贝
var obj1 = {
	"name":"zhangsan",
    "age":18,
    "language":["Chinese",["English","American"],"French"]
}
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[1] = ["Korean","Japanese"];
function shallowCopy(obj){
	var dst = {};
    for(prop in obj){
    	if(obj.hasOwnProperty(prop)){
        	dst[prop] = obj[prop];
        }
    }
    return dst;
}
console.log(obj1,obj3);

结果如下图所示:
在这里插入图片描述

浅拷贝对于基本类型的对象(或属性)操作后相当于深拷贝(重新开辟一块内存空间),但是对于引用类型的对象(或属性)则只是相当于赋值操作,即浅拷贝对于引用类型只是保存栈中指向堆内容的地址。(Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。)

从案例一和案例二可知,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:
图片描述

四、浅拷贝的实现方式

1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

案例三

var obj1 = {
	"name":"zhangsan",
    "age":18,
    "language":["Chinese",{"name":"zy"},"French"]
}
var obj4 = Object.assign({},obj1);
obj4.name = "zy";
obj4.language[1].name = "chaopy";
console.log(obj1,obj4);

结果如下图所示:
在这里插入图片描述

要点:对于引用类型的属性属于浅复制。修改其中一个会对两一个产生影响。

案例四

var obj1 = {
	"name":"zhangsan",
    "age":18,
    "language":["Chinese",["English","American"],"French"]
}
var obj4 = Object.assign({},obj1);
obj4.name = "lisi";
console.log(obj1,obj4);

结果如下图所示:
在这里插入图片描述

注意:当object只有一层的时候,是深拷贝。

2.Array.prototype.concat()

concat()方法用于合并两个或多个数组。此方法不改变原数组,而是返回一个新数组。参数是数组或者值,返回值是新数组。concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,因此要注意引用类型的数据如果其属性改变,对于原数组和新数组都是可见的。

案例五

var obj1 = ["Chinese",{"name":"zy"},"French"];
var obj5 = obj1.concat();
obj5[1].name = "love";
obj5[2] = "Japanese";
console.log(obj1,obj5);

结果如下图所示:
在这里插入图片描述

要点:对于引用类型的元素属于浅拷贝(修改新对象会改到原对象)。对于原始类型的属性属于深拷贝。

3.Array.prototype.slice()

arrayObject.slice(start,end)方法返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

案例六

var obj1 = ["Chinese",{"name":"zy"},"French"];
var obj6 = obj1.slice();
obj6[1].name = "love";
obj6[2] = "Japanese";
console.log(obj1,obj6);

显示结果如图所示:
在这里插入图片描述
要点

关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中每个元素的一个新数组。 原数组的元素会按照下述规则拷贝:
如果该元素是引用对象(非原始对象),slice会拷贝这个对象的引用到新的数组里。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

五、深拷贝的实现方式

1、JSON.parse(JSON.stringify())

案例七

var arr1 = [1,3,{"userName":"zy"}];
var arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].userName = "chaopy";
console.log(arr1,arr2);

显示结果如下图所示:
在这里插入图片描述

原理:
用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象的深拷贝,但不能处理函数(如案例八所示)。

案例八

var arr1 = [1,3,{"userName":"zy"},function(){}];
var arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].userName = "chaopy";
console.log(arr1,arr2);

显示结果如下所示:
在这里插入图片描述

这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

函数库lodash

Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高JS原生方法性能的JS库。简单的说就是,很多方法lodash已经帮你写好了,直接调用就行,不用自己费尽心思去写了,而且可以统一方法的一致性。Lodash使用了一个简单的 _ 符号,就像Jquery的 $ 一样,十分简洁。lodash函数库提供 _.cloneDeep用来做深拷贝。

案例九

function loadJS( url, callback ){
    var script = document.createElement('script'),fn = callback || function(){};
    script.type = 'text/javascript';
    if(script.readyState){//IE
        script.onreadystatechange = function(){
            if( script.readyState == 'loaded' || script.readyState == 'complete' ){
                script.onreadystatechange = null;
                fn();
            }
        };
    }else{ //其他浏览器
        script.onload = function(){
            fn();
        };
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}
var lodashUrl = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js";
loadJS(lodashUrl,lodashLoadSuccess);
function lodashLoadSuccess(){
	var obj1 = {
		a:1,
		b:{f:{g:1}},
		c:[1,2,3],
		d:function(){console.log("I am a Function");}
	};
	var obj2 = _.cloneDeep(obj1);
	obj2.a = 2;
	obj2.b.f.g = 3;
	obj2.c[2] = 4;
	console.log(obj1,obj2);
	console.log("(obj1.b.f===obj2.b.f) = "+(obj1.b.f === obj2.b.f));
}

显示结果如下图所示:
在这里插入图片描述
lodash中的cloneDeep()方法可以对所有类型的对象做深拷贝,包括函数对象。
在这里插入图片描述
用lodash的cloneDeep()进行深拷贝后的对象拥有独立的内存空间,修改其中一个不会影响到另一个。

手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
图片描述

参考文献: 链接:https://www.jianshu.com/p/d46abfa4ddc9

发布了33 篇原创文章 · 获赞 35 · 访问量 812

猜你喜欢

转载自blog.csdn.net/chaopingyao/article/details/105026649