说深拷贝与浅拷贝之前,先来看一看js中有哪些数据类型,有助于对后面知识的理解。结合前辈们及自己的理解总结了一下,如有出入,欢迎指正!
1、js中包含两种不同数据类型的值:基本类型
和引用类型
。
基本类型值指的是简单的数据段,包括es6里面新增的一共是有6种,具体如下:number、string、boolean、null、undefined、symbol。
引用类型值指那些可能由多个值构成的对象,只有一种如下:object。
在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。
2、javascript的变量的存储方式:栈(stack)
和堆(heap)
栈:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址
堆:动态分配的内存,大小不定,也不会自动释放。里面存放引用类型的值。
基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。
引用类型的值是保存在内存中的对象。
注意:JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。 在操作对象时, 实际上是在操作对象的引用而不是实际的对象。
3、基本类型与引用类型最大的区别实际就是 传值与传址 的区别
值传递:基本类型采用的是值传递。
地址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量。
浅拷贝
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
只拷贝数值,不拷贝地址。节省内存,但相互影响。
//对基本数据类型Number(赋值操作)
let a=1;
let b=a;
console.log(b); //1
b=2;
console.log(b); //2
console.log(b); //1
//对数组操作
let arr1 = [1,2,3];
let arr2 = arr1;
console.log(arr2); //[1,2,3]
arr2.push(4);
console.log(arr2); //[1,2,3,4]
console.log(arr1); //[1,2,3,4]
/*首先栈内存arr1会指向堆内存里的数组,栈内存的arr1保存的是数组的引用,
也就相当于内存地址,arr2=arr1,会把arr1的引用赋给arr2,所以arr2也有了数组的引用,
此时arr1和arr2指向的是同一个数组,因此一个数组的改变会影响另一个数组的值。*/
//对对象操作
let obj1={count:1,name:'grace',age:1};
let obj2 = obj1;
console.log(obj2); //{count:1,name:'grace',age:1}
obj2.count=2;
console.log(obj1); //{count:2,name:'grace',age:1}
console.log(obj2); //{count:2,name:'grace',age:1}
/*综上所述,如果是基本数据类型,直接进行赋值操作,这样就相当于在栈内存中重新开辟了一个新的空间把值传递
过去;如果是引用类型的值传递,进行的就是浅拷贝,浅拷贝赋值的只是对象的引用,如上述obj2=obj1,实际上
传递的只是obj1的内存地址,所以obj2和obj1指向的是同一个内存地址,所以这个内存地址中值的改变对obj1和
obj2都有影响。*/
深拷贝
深拷贝不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上,
所以对一个对象的修改并不会影响另一个对象。
数值和地址都拷贝。 内存独立,不会相互影响
//对数组操作
//方法一:for循环
let arr1 = [1,2,3];
let arr2 = copyArr(arr1);
function copyArr(arr){
let res=[];
for(let i=0,length=arr.length;i<length;i++){
res.push(arr[i]);
}
return res;
}
//方法二: slice
//利用数组自身所带的方法,截取一个新的数组。可以翻看在下那一篇讲数组及其方法的文章,有介绍具体用法
let arr1 = [1,2,3];
let arr2 = arr1.slice(0);
//方法三: concat
//同样是数组自身所带的方法,将两个数组拼接成一个新数组
let arr1 = [1,2,3];
let arr2 = arr1.concat();
//方法四:扩展运算符(es6中推出的,很好用)
let arr1 = [1,2,3];
let [...arr2] = arr1;
//方法五:Array.from
//如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组
let arr1 = [1,2,3];
let arr2 = Array.from(arr1);
console.log(arr2);//[1,2,3]
//对对象操作
//方法一:for循环
let obj1={
name:'Tom',
age:20
};
let obj2 = copyObj(obj1);
function copyObj(obj){
let newObj = {};
for(let key in obj){
newObj[key]=obj[key];
}
return res;
}
//方法二:利用JSON.parse()方法
// 解决传址 相互影响的问题
// 这里会返回一个新的对象,达成深拷贝的效果
// 但是这种方式,会丢失function函数 和 undefined
let obj1={
count:1,
name:'grace',
age:1
};
let obj2 = JSON.parse(JSON.stringify(obj1));
//方法三:扩展运算符
let obj1={
count:1,
name:'grace',
age:1
};
let {...obj2} = obj1; console.log(obj2);//{count:1,name:'grace',age:1};
//合成版,可以实现数组和对象的深拷贝
function deepCopy(obj){
let result = Array.isArray(obj)?[]:{};
//for in 遍历对象和原型链上的属性和方法
for(let key in obj){
// 判断是否是对象自身的内容,如果是,才去做深拷贝
// 对象的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身非继承属性(自身的)。
if(obj.has OwnProperty(key)){
//判断是否是对象
if(obj[key]&&typeof obj[key]==='object'){
//如果内部依然有更深层次的对象
//那么这里还需要按照原有步骤,再循环检测一遍
result[key]=deepCopy(obj[key]);
}
//如果内部没有对象,就直接赋值给新对象
else{
result[key]=obj[key];
}
}
}
//最后,将我们检测赋值后的新对象返回,以此达成新内存的目的
return result;
}
let obj = {
name:"Jone",
age:20,
sex:"男",
text:undefined,
play:function(){
console.log("跑步");
}
}
let obj2 = deepCopy(obj);
console.log(obj2);
obj2.play();
浅拷贝与深拷贝的区别:
(1)浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,
所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign()下面有介绍)
(2)深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝
(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)
当你需要深拷贝对象中的方法时是可以用lodash.js(提高JS原生方法性能的JS库)中的cloneDeep()方法
<script type="text/javascript">
var objA = {
"name": "戈德斯文"
};
var objB =lodash.cloneDeep(objA);
</script>
ES6新增了Object.assign(target,source1,source2…) 方法
target:第一个参数是目标对象,
source1,source2…:一个或多个源对象。
它会遍历一个或多个源对象可枚举的键并把它们复制到目标对象,最后返回目标对象
assign是使用=操作符来赋值,
Object.assign() 只是一级属性复制,比浅拷贝多深拷贝了一层而已。
<script>
let obj = {
name: "张三",
age: 20,
sex: "男",
text: undefined,
play: function () {
console.log("跑步");
}
}
let obj2 = Object.assign(obj);
console.log(obj2);
let target = {a:1};
let source1 = {b:2};
let source2 = {c:3};
target = Object.assign(target,source1,source2);
console.log(target);//{a:1,b:2,c:3}
// 如果目标对象与源对象有同名属性,或多个源对象有同名属性,前面的属性会被后面的覆盖
let t1 = {num:10};
let s1 = {txt:"aa"};
let s2 = {txt:"cc"};
console.log(Object.assign(t1,s1,s2));//{num: 10, txt: "cc"}
</script>