【js进阶】-深浅拷贝
一、为什么会出现深浅拷贝
实质上是由于JS对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象,而JS不允许我们直接操作内存中的地址,也就是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
二、js中复制初体验
当我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西(也就是值)并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。
var num = 10;
var num2 = num;
var obj = {
name: 'Nicholas'
}
var obj2 = obj;
obj.name = 'Lee';
obj2.name; // 'Lee'
解析:var num2 = num; 属于基本类型的复制,直接在栈内存中创建了一块新内存空间给num2,存的值同样是10,num2和num完全无关,而var obj2 = obj因为obj是一个对象,所以属于引用类型的复制,所以此时复制给obj2的只是原先保存在obj变量中的引用地址(指针)而已,此操作过后,obj和obj2两个变量存的都是堆内存那个实际对象的引用地址,两个变量指向了同一个内存空间,所以当obj修改对象的name属性时,其实改的是堆内存中的那个对象,由于obj2和obj指向的是同一个对象,所以打印obj2.name就是修改后的那个name值了
三、js中的深浅拷贝(外加首层浅拷贝)
注意:我们所说的深浅拷贝一般用于引用类型的复制,不用于基本类型复制
浅拷贝:
只复制了引用而未真正复制值
几种浅拷贝的方式:
1、“ = ”运算符
const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneArray = originArray;
const cloneObj = originObj;
console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}
cloneArray.push(6);
cloneObj.a = {aa:'aa'};
console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]
console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray 和 originObj 也随着发生了变化。
深拷贝:
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了,只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
实现深拷贝的方式:
1、利用 JSON 对象中的 parse 和 stringify
---------------------------数组的深拷贝-----------------------------
const arr1 =[1,2,3,4,5]
const arr2 =JSON.parse(JSON.stringify(arr1)) //深拷贝
console.log(arr2) //[1,2,3,4,5]
arr1.push(6)
console.log(arr1) //[1,2,3,4,5,6] //添加了6
console.log(arr2) //[1,2,3,4,5] 还是原先的值,两者不相关
---------------------------对象的深拷贝-----------------------------
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
以上情况确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:
const originObj = {
name:'haha',
sayHello:function(){
console.log('Hello haha');
}
}
console.log(originObj); // {name: "haha", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "haha"}
发现在 cloneObj 中,有属性丢失了。。。那是为什么呢?
结论:因为使用JSON.parse/stringify在遇到函数、undefined、Symbol、正则等时会丢失,
new Date会被转成时间字符串形式,无法对上述几种情况进行正常复制,所以当遇到要复制的对象中包含函数的时候,就不能使用JSON.parse/stringify进行深拷贝了
2、利用递归来实现每一层都重新创建对象并赋值
function deepClone(source){
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
好的,我们来试试
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
ok。那再试试带有函数的:
const originObj = {
name:'axuebin',
sayHello:function(){
console.log('Hello World');
}
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}
也ok
首层浅拷贝:
这里相信有好多同学都是第一次听到这个词汇,我们来解释下到底什么是首层浅拷贝,其实就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,这就称作“首层浅拷贝”。
实现首层浅拷贝的方式:
1、数组的concat()方法
-------------------简单情况都是基本类型时------------------
const originArray = [1,2,3,4,5]; //一层数组
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];
----------------------------有引用类型时---------------------------------
const originArray = [1,[1,2,3],{a:1}]; //多层数组
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[0]=2;
console.log(cloneArray) // [2,[1,2,3],{a:1}];
console.log(originArray) // [1,[1,2,3],{a:1}]; 修改cloneArray的第一个值不影响原数组
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}] //修改cloneArray的数组和对象值时会影响原数组,说明两者的引用是同一个
结论:concat 只是对数组的第一层进行深拷贝。
2、slice
-------------------简单情况都是基本类型时------------------
const originArray = [1,2,3,4,5];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5]; //两者互不干扰,是深拷贝
-------------------有引用类型时------------------
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[0] =2; //修改克隆数组的第一个值
console.log(cloneArray) // [2,[1,2,3],{a:1}];
console.log(originArray) // [1,[1,2,3],{a:1}]; 修改cloneArray的第一个值不影响原数组
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}] //修改cloneArray的对象和数组则会影响原数组,说明引用的是同一个对象和数组
结论:slice 只是对数组的第一层进行深拷贝。
3、Object.assign()
-------------------简单情况都是基本类型时------------------
let srcObj = {'name': 'lilei', 'age': '20'};
let copyObj2 = Object.assign({}, srcObj);
console.log('srcObj', srcObj); //'name': 'lilei', 'age': '20'
console.log('copyObj2', copyObj2); //'name': 'lilei', 'age': '20'
srcObj.name="zhangsan";
console.log('srcObj', srcObj); //'name': 'zhangsan', 'age': '20'
console.log('copyObj2', copyObj2); //'name': 'lilei', 'age': '20'
copyObj2.age="10";
console.log('srcObj', srcObj); //'name': 'zhangsan', 'age': '20'
console.log('copyObj2', copyObj2); //'name': 'lilei', 'age': '10'
---------------------------有引用类型时----------------------------
let srcObj = {'name': 'lilei', 'grade': {'chi':"80", 'eng':"100"}};
let copyObj2 = Object.assign({}, srcObj);
copyObj2.name="zhangsan";
copyObj2.grade.chi="50";
console.log('srcObj', srcObj); //name: "lisi" grade: {chi: "50", eng: "100"}
console.log('copyObj2', copyObj2); //name: "zhangsan" grade: {chi: "50", eng: "100"}
结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
4、… 展开运算符
const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};
const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]
const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}
结论:… 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。
四、总结
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
- JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
- JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
- 若想真正意义上的深拷贝,请递归。