浅拷贝与深拷贝
为了更好的理解js的深浅拷贝,我们先来理解一些js基本的概念
##数据类型 javascript中的数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和复杂的数据类型(也称作引用数据类型-Object)。
当一个变量存放基本数据类型时与复杂的数据类型时分别存在以下的特点:
基本数据类型的特点:变量直接将基本数据类型的值存储在栈(stack)内存中 引用数据类型的特点:变量将引用数据类型的引用(可以看作是地址)存储在栈(stack)内存,而对象本身存放在堆内存里,在栈中引用指向堆中的对象。
其实当解释器寻找引用值时,会首先检索其在栈中的地址引用,取得地址后从堆中获得对象。
首先我们来看一个例子:
var num = 666; var numCopy = num; var obj = {name:"张三"}; var objCopy = obj;
对于以上的代码,变量的内存分配是在栈中,所以变量不可以存放堆中的对象,所以赋值就会出现两种情况:
基本数据类型:将变量num的值拷贝一份存储在numCopy中
赋值数据类型:将变量obj中直接存储的引用拷贝一份存储在objCopy中
所以我们的Number类型的变量num通过简单的赋值得到了两份值(在栈内存不同的内存区域),而复杂数据类型obj通过赋值仅仅是得到了两份引用(在栈内存不同的内存区域),而实际的对象(在堆内存中是同一个值)只有一份。
所以我们可以知道,要拷贝一份复杂数据类型远没有我们想象的那样简单。
这也就引出了我们今天的话题,深拷贝和浅拷贝。
我们应该要注意一点,深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。而简单数据类型由于情况和简单就是拷贝一份,所以不存在深拷贝和浅拷贝的说法。
那我们来理解一下深拷贝和浅拷贝吧。
赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的对象本身,因此,两个对象是联动的。
浅拷贝:它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。但是属性值的拷贝是通过赋值来完成的。也就存在我们之前讨论的问题,如果属性是基本数据类型,拷贝的就是基本数据类型的值;如果属性是复杂数据类型,拷贝的就是引用 ,因此如果新旧对象其中一个改变了这个复杂数据类型的属性的对象本身,就会影响到另一个对象。所以仅仅是表面上得到了一个新的对象,其实内部的复杂数据类型还是原来的,会存在复杂数据类型的属性的引用相等的情况(共享同一个对象),所以叫做"浅"拷贝。
示例:
var num = 666; var numCopy = num; var obj = {name:"张三",dog:{name:"小黑"}}; var objCopy = obj; //浅拷贝obj得到objShallowCopy var objShallowCopy = {}; objShallowCopy.name = obj.name; objShallowCopy.dog = obj.dog; //修改通过赋值拷贝的内容 numCopy = 777; console.log(num,numCopy);//666 777 objCopy.name = "李四"; console.log(obj,objCopy);//{name:"李四",dog:{name:"小黑"}}; {name:"李四",dog:{name:"小黑"}}; //修改通过浅拷贝的拷贝内容 objShallowCopy.name = "王五"; objShallowCopy.dog.name = "小花"; console.log(obj,objShallowCopy);//{name:"李四",dog:{name:"小花"}}; {name:"王五",dog:{name:"小花"}};
通过上面的示例我们可以知道:
-
基本数据类型的值通过赋值,在内存中存在两个备份,互不影响;
-
复杂数据类型的值通过赋值,引用得到了两份,但是对象本身只有一份,改变其中一个,另一个也会改变(因为就是同一个对象,所以叫改变也不贴切)
-
复杂数据类型的值通过浅拷贝,最外层对象拷贝得到了一个新的对象,但是内部的复杂数据类型的属性的对象还是原来对象引用指向的那一个,只拷贝了引用,所以修改复杂数据类型的属性dog上的name为"小花",新旧对象都变成了小花。
浅拷贝和深拷贝对比
深拷贝:深拷贝会另外创造一个一模一样的对象,新对象跟原对象"完全"不共享堆内存,修改新对象不会改到原对象。也就是说不管原对象有多么复杂,深拷贝会将其及其属性(属性下的属性…..)都备份一份,不会出现浅拷贝那样,值是一模一样,但是其中存在相同引用的情况。
我们通过代码来得到上面obj的一个深拷贝:
var obj = {name:"张三",dog:{name:"小黑"}}; var objDeepCopy = {}; objDeepCopy.name = obj.name; var newDog = {}; newDog.name = obj.dog.name; objDeepCopy.dog = newDog; //这里我们修改一下复杂数据类型属性dog的name属性 objDeepCopy.dog.name = "小花"; console.log(obj,objDeepCopy);//{name:"李四",dog:{name:"小黑 {name:"王五",dog:{name:"小花"}};
所以我们可以这样认为,浅拷贝只是肤浅的拷贝一个对象,而不拷贝对象的所有,新旧对象存在共享同一块堆内存的情况。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享堆内存,修改新对象不会改到原对象。
深浅拷贝的代码实现
浅拷贝
1.Array中存在一些可以实现浅拷贝的方法:slice、concat...
var arr = [1,2,[3,4]]; var newArr = arr.slice(0,arr.length); //var newArr = arr.concat(); console.log(arr,newArr);//[1,2,[3,4]] [1,2,[3,4]] :首先数据显示一模一样 console.log(arr == newArr);//false :false说明外层拷贝了 newArr[2][0] = 99; console.log(arr,newArr);//[1,2,[99,4]] [1,2,[99,4]] :都是99说明内层没有拷贝
2.一般的对象可以用Object的方法:assign
var obj = {name:"张三",dog:{name:"小黑"}}; var newObj = Object.assign({},obj); console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小黑"}}; console.log(obj == newObj);//false newObj.dog.name = "小花"; console.log(obj,newObj);//{name:"张三",dog:{name:"小花"}} {name:"张三",dog:{name:"小花"}};
3.我也可以使用ES6提供的新的语法(对象和数组都可以使用):"..."
var obj = {name:"张三",dog:{name:"小黑"}}; var newObj = {...obj} console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小黑"}}; console.log(obj == newObj);//false newObj.dog.name = "小花"; console.log(obj,newObj);//{name:"张三",dog:{name:"小花"}} {name:"张三",dog:{name:"小花"}};
var obj = {name:"张三",dog:{name:"小黑"}}; var newObj = JSON.parse(JSON.stringify(obj)); console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小黑"}}; console.log(obj == newObj);//false newObj.dog.name = "小花"; console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小花"}};
有一个缺点,就是该方法会忽略undefined、任意的函数、symbol 值
,因为JSON不支持这些数据类型
2.使用递归实现
function deepClone(obj){ let result = Array.isArray(obj)?[]:{}; if(obj && typeof obj === "object"){ for(let key in obj){ if(obj.hasOwnProperty(key)){ if(obj[key] && typeof obj[key] === "object"){ result[key] = deepClone(obj[key]); }else{ result[key] = obj[key]; } } } } return result; } var obj = {name:"张三",dog:{name:"小黑"}}; var newObj = deepClone(obj); console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小黑"}}; console.log(obj == newObj);//false newObj.dog.name = "小花"; console.log(obj,newObj);//{name:"张三",dog:{name:"小黑"}} {name:"张三",dog:{name:"小花"}};
但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。如:
var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepClone(obj1);
由于obj1中有一个属性z指向obj1(即他自己,递归会无限执行下去),这里的改进我们就不再继续深入讨论了。
深比较和浅比较
相关的东西,那就是深浅比较。同样的,所谓的深浅也只是针对于复杂数据类型,它们都是用来判断两个对象是否相等的。
浅比较
浅比较只判断一层,用来判断两个对象内部的的属性是否是一样的,其中属性值的比较使用"===",所以其中属性时复杂数据类型时,其实做的是引用比较:
function isObjEqual (o1, o2) { var props1 = Object.getOwnPropertyNames(o1); var props2 = Object.getOwnPropertyNames(o2); if (props1.length != props2.length) { return false; } for (var i = 0,max = props1.length; i < max; i++) { var propName = props1[i]; if (o1[propName] !== o2[propName]) { return false; } } return true; }
深比较
深比较也称原值相等,深比较是指检查两对象的所有属性是否都相等,深比较需要以递归的方式遍历两个对象的所有属性,也就是说,当属性是复杂数据类型时,还会递归的再次深比较该复杂数据类型的值,操作比较耗时,深比较不管这两个对象是不是同一对象的引用。
深比较和浅比较对比
总的来说,深比较能够更彻底的递归完一个对象,如果属性的引用不相等,还会递归该属性去看属性中的内容相等与否,而浅比较一旦出现复杂数据类型的属性,只要引用不是同一个引用,即判定两个对象不相等。所以在不同要求选择使用不同的比较方式才是合理的。