目录
在练习代码时,我遇到了这样一个问题:当我把一个数组赋值给另外一个数组,用另外一个数组进行数据元素处理,输出结果时,发现原数组的数据元素随之发生相同改变。通过搜集资料,了解了这是由于浅拷贝造成的结果。故在此对于浅拷贝和深拷贝进行一个学习总结。由于原本练习代码较长,这里简约演示一下:
var arr1=[1,2,3,4,5]; //定义数组arr1=[1,2,3,4,5]
console.log("arr1=",arr1); //输出数组arr1= [ 1, 2, 3, 4, 5 ]
var arr2=[]; //定义空数组arr2
arr2=arr1; //将数组arr1赋值给arr2
console.log("arr2=",arr2); //输出赋值后的数组arr2=[1,2,3,4,5]
for(var i=0;i<arr2.length;i++){ //将数组arr2逆置
for(var j=i;j>0;j--){
if(arr2[j-1]<arr2[j]){
var temp=arr2[j-1];
arr2[j-1]=arr2[j];
arr2[j]=temp;
}
}
}
console.log("arr2=",arr2); //输出逆置后的数组arr2= [ 5, 4, 3, 2, 1 ]
console.log("arr1=",arr1); //输出数组arr1= [ 5, 4, 3, 2, 1 ]
arr1[0]=9; //改变数组arr1第一个元素为9
console.log("arr1=",arr1); //输出数组arr1= [ 9, 4, 3, 2, 1 ]
console.log("arr2=",arr2); //输出数组arr2= [ 9, 4, 3, 2, 1 ]
通过上面的代码可以发现,将数组arr1赋值给数组arr2后,对数组arr2内的元素进行逆置,可是arr1中的元素也随之改变,元素逆置,这就是浅拷贝,具体的下面进行分析
浅拷贝
含义
浅拷贝是创建一个新对象,并拷贝原始对象属性。若原始对象属性是基本类型,拷贝的即是基本类型值,若原始对象属性是引用类型,拷贝的则是内存地址。故如果其中一个对象改变了这个对象的地址,就会影响到另一个对象(注意:浅拷贝只拷贝了数据对象的第一层,深层次的数据值和原始数据会互相影响。)
基本类型浅拷贝
基本类型:简单的数据段,常见的有Number、String 、Boolean、Null和Undefined,基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值
//基本类型浅拷贝
var a=1;
var b=a;
b=2;
console.log("a="+a); //输出a=1
console.log("b="+b); //输出b=2
由上例可以看见,将a拷贝给b,b值改变对a没有影响,因为b只是保存了a复制的一个副本,虽然两个变量值相等,但是分别保存了两个不同的基本数据类型值,所以b改变对a没有影响,我们可以用图来演示基本数据类型赋值过程:
引用类型浅拷贝
引用类型:由多个值构成的对象,也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。引用数据类型时保存在堆内存中的对象,不能直接访问堆内存空间位置和操作堆内存空间,只能操作对象在栈内存中的引用地址。
//引用类型浅拷贝
var obj1=new Object();
var obj2=obj1;
obj2.name="zhangsan";
console.log("obj1=",obj1.name); //输出obj1= zhangsan
console.log("obj2=",obj2.name); //输出obj2= zhangsan
由上例可以看见,将obj1赋值给obj2,改变obj2时,obj1也随之发生改变 。说明两个引用数据类型指向了同一个堆内存对象,我们可以理解为新对象和原对象共享内存,故其中一个对象改变其内存内容,其余对象都会随之发生改变。同样用图演示一下:
常见实现方式
上述已经阐明了引用类型和基本类型的区别,所以接下来都是以引用类型浅拷贝为例
1.Object.assign()
语法:Object.assign(target,...sources)
ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources(可以是多个)
//Object.assign()
//浅拷贝只拷贝了数据对象的第一层,深层次的数据值和原始数据会互相影响
let obj1={name:"zhangsan",age:{num:20}};
let obj2=Object.assign({},obj1);
obj2.name="lirong";
obj2.age.num=18;
console.log(obj1); //{ name: 'zhangsan', age: { num: 18 } }
console.log(obj2); //{ name: 'lirong', age: { num: 18 } }
Object.assign()注意事项:
1.不拷贝对象不可枚举属性
2.只拷贝源对象自身属性,不拷贝继承属性
3.undefined和null无法转为对象,不能作为Object.assign参数,可作为源对象
2....扩展运算符
语法:var cloneObj={...obj}
该运算符是一个ES6/ES2015特性,与 Object.assign ()的功能相同
//...扩展运算符
let obj1={name:"zhangsan",age:{num:20}};
let obj2={...obj1};
obj2.name="lirong";
obj2.age.num=18;
console.log(obj1); //{ name: 'zhangsan', age: { num: 18 } }
console.log(obj2); //{ name: 'lirong', age: { num: 18 } }
3. Array.prototype.concat()
语法:var newArray=oldArray.concat(value1[,value2[,...[,valueN]]])
参数:连接成新数组
//Array.prototype.concat()
let arr=[{a:1},{b:2}];
let arr1=[{c:3},{d:4}];
let arr2=arr.concat(arr1);
arr1[1].d=9;
console.log(arr2); //[ { a: 1 }, { b: 2 }, { c: 3 }, { d: 9 } ]
console.log(arr1); //[ { c: 3 }, { d: 9 } ]
4. Array.prototype.slice()
语法:var newArray=Array.prototype.slice.call(oldArray);
//Array.prototype.slice()
let arr1=[{a:1},{b:2}];
let arr2=Array.prototype.slice.call(arr1);
arr2[1].b=5;
console.log(arr1); //[ { a: 1 }, { b: 5 } ]
console.log(arr2); //[ { a: 1 }, { b: 5 } ]
深拷贝
含义
深拷贝时将一个对象从内存中完整拷贝一份出来,从堆内存中开辟一个新区域存放新对象,两者不会共享内存,故修改新对象不会影响原对象。
如果用上述引用类型浅拷贝与深拷贝做对比的话,可以这样作图:
常见实现方式
1.JSON.parse(JSON.stringify())
原理:利用JSON.stringify()把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串形式保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新对象(此方法可以处理数组或对象深拷贝,但是不能处理函数和正则)
//JSON.parse(JSON.stringify())
let obj1={name:'zhangsan',info:{age:22},fun:function(){}}
let obj2=JSON.parse(JSON.stringify(obj1));
obj2.name='lirong';
obj2.info.age='18';
console.log(obj1); //{ name: 'zhangsan', info: { age: 22 }, fun: [Function: fun] }
console.log(obj2); //{ name: 'lirong', info: { age: '18' } }
- JSON.stringify()实现深拷贝注意点
1.无法拷贝不可枚举属性
2.无法拷贝对象原型链
3.无法拷贝对象的循环应用(例如obj[key]=obj)
4.拷贝Date引用类型会变成字符串
5.拷贝RegExp引用类型会变成空对象
6.若对象中含有NaN、Infinity和-Infinity,序列化结果变成null
7.若拷贝对象值中含有函数、undefined、symbol,经过JSON.stringify()序列化后,JSON字符串中这个键值对消逝
2.循环遍历赋值
该方法大多用于数组
//循环遍历拷贝
let arr1=[1,2,3,4,5];
let arr2=[];
for(let i=0;i<arr1.length;i++){
arr2[i]=arr1[i]
}
arr2[0]=9;
console.log(arr2); //[ 9, 2, 3, 4, 5 ]
console.log(arr1); //[ 1, 2, 3, 4, 5 ]
3.递归方法
用递归方法实现深度克隆原理:遍历对象、数组指导里面都是基本数据类型
//递归方法
var obj={
a:'普通属性',
arr:[1,2,3],
fn:function(){
console.log("方法");
}
}
function deepClone(obj){
let newObj;
if(typeof obj === 'object'){ //typeof运算符检测obj变量类型
newObj=Array.isArray(obj)?[]:{}; //Array.isArray()用于确定传递的值是否是一个Array
for(let key in obj){
if(obj.hasOwnProperty(key)){ //hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性
newObj[key]=obj[key];
}else{
newObj[key]=deepClone(obj[key])
}
}
}else{ //普通类型,直接赋值
newObj=obj;
}
return newObj;
}
var deepClone=deepClone(obj);
console.log(deepClone); //{ a: '普通属性', arr: [ 1, 2, 3 ], fn: [Function: fn] }
obj.fn='change';
console.log(obj); //{ a: '普通属性', arr: [ 1, 2, 3 ], fn: 'change' }
console.log(deepClone); //{ a: '普通属性', arr: [ 1, 2, 3 ], fn: [Function: fn] }
除了上述方法,还有其他方法可以实现深拷贝,例如利用函数库lodash的_.cloneDeep方法还有jQuery.extend()方法等,由于目前学习进度还没有到这里,所以暂时总结两种深拷贝方法,之后进行补充。