什么是深拷贝?什么是浅拷贝?
大家初学js的时候,听到别人在说深拷贝、浅拷贝的时候肯定会觉得很好奇,这到底是个啥?不急,我们通过一个例子来介绍深浅拷贝的区别。
拷贝,顾名思义就是复制一份,我跟你得一样,这就是拷贝。下面我们用ES6的拓展运算符实现一下拷贝。可以看到,arr和arr2的内容是一样的。
let obj = {
name: "jzsp",
age: 20,
};
let arr = [1, 2, 3, obj];
let arr2 = [...arr];
console.log(arr); //[ 1, 2, 3, { name: 'jzsp', age: 20 } ]
console.log(arr2); //[ 1, 2, 3, { name: 'jzsp', age: 20 } ]
但是这是一个浅拷贝,什么是浅拷贝呢?深浅拷贝的根本差别是引用类型的数据。我们都知道引用类型是存在堆内存中的,上面的例子中,数组保存了一个引用类型obj,保存的其实是这个引用类型的地址。我们如果修改arr中的obj,看看arr2会有什么变化。
let obj = {
name: "jzsp",
age: 20,
};
let arr = [1, 2, 3, obj];
let arr2 = [...arr];
arr[3].name = 'jzsp2号'
console.log(arr); //[ 1, 2, 3, { name: 'jzsp2号', age: 20 } ]
console.log(arr2); //[ 1, 2, 3, { name: 'jzsp2号', age: 20 } ]
\
可以看到,修改了其中一个引用的属性,那么另一个引用也发生了变化,这是因为他们本质上指向的是同一个对象。大概如下图一样(画的有点joker)
深拷贝的含义就是,引用类型我也要单独复制一份,这样的话修改其中一个的引用,另外一个拷贝的对象不会发生变化,大概如下图
实现浅拷贝的方法
...
...指的不是无语,是es6的展开运算符。例子就像我上面的那个一样
let arr = [1, 2, 3];
let arr2 = [...arr]; //浅拷贝数组
let obj = {
name:'jzsp',
age:20
}
let obj2 = {...obj} //浅拷贝对象
console.log(obj2)
Object.assign
let arr = [1, 2, 3];
let arr2 = Object.assign([],arr) //浅拷贝数组
console.log(arr2) //[1,2,3]
let obj = {
name:'jzsp',
age:20
}
let obj2 = Object.assign({},obj) //浅拷贝对象
console.log(obj2) //{ name: 'jzsp', age: 20 }
实现深拷贝的方法
JSON
我们可以先通过JSON.stringify()将对象转成JSON字符串,再通过JSON.parse将JSON字符串转成对象。由下图看到,深拷贝确实不会被影响,浅拷贝修改引用类型的值会对源对象有影响。
let obj = {
name:'jzsp',
arr:[1,2,3,4]
}
let obj2 = { //浅拷贝
...obj
}
let obj3 = JSON.parse(JSON.stringify(obj)) //深拷贝
obj2.arr[0] = 3
console.log(obj)
console.log(obj2)
console.log(obj3)
但是JSON字符串化是有一定规则的,不符合JSON安全值的内容会被忽略甚至报错(undefined,function,symbol),以及循环引用(对象之间的互相引用,形成一个无限循环)
let obj = {
name:undefined,
foo:function(){
console.log(81)
},
[Symbol(123)]:'123'
}
let obj2 = {
...obj
}
let obj3 = JSON.parse(JSON.stringify(obj))
console.log(obj)
console.log(obj2)
console.log(obj3)
可以看到,不安全的JSON值在拷贝的时候都被忽略了。
那么接下来,我们需要自己实现一个深拷贝函数。
自己实现深拷贝函数
自己实现的深拷贝主要是需要解决下面几个问题:
- function被忽略
- symbol被忽略
- undefined被忽略
- 循环引用
1、深拷贝初步实现
let obj = {
name:undefined,
foo:function(){
console.log(81)
},
[Symbol(123)]:'123',
arr:[1,2,3,4]
}
//判断是不是对象类型
function isObject(obj){
let type = typeof obj
return obj !== null && (type === "object" || type === "function");
}
function deepClone(val){
//val是需要拷贝的内容,如果这个需要拷贝的内容不是对象类型,就直接返回即可
if(!isObject(val)){
return val
}
//是对象类型,那么我们要创建一个新的对象,将内容拷贝到这个对象上并且返回
let newObj = {}
for(const key in val){
//因为我们不知道val对象的key属性是值还是对象,所以我们可以递归的调用方法对这个值做同样的处理
newObj[key] = deepClone(val[key])
}
return newObj
}
let obj2 = deepClone(obj)
console.log(obj2)
console.log(obj)
2、对数组类型进行处理
可以看到,值为数组的属性被拷贝成了对象,所以我们需要对数组进行判断(下面我就只写deepClone中的内容,并且重复注释就去掉拉)
function deepClone(val) {
if (!isObject(val)) {
return val;
}
//如果值是数组的话,就创建数组,否则就创建对象
let newObj = Array.isArray(val) ? [] : {};
for (const key in val) {
newObj[key] = deepClone(val[key]);
}
return newObj;
}
3、对函数对象进行处理
函数的内容被拷贝成了一个空对象,这里有个问题:我们需要将函数重新拷贝一份吗?显然是没有必要且做不到的,所以我们判断值是函数类型的时候直接返回即可
function deepClone(val) {
if (!isObject(val)) {
return val;
}
//如果是函数对象,直接返回这个函数对象
if(typeof val === 'function') return val
let newObj = Array.isArray(val) ? [] : {};
for (const key in val) {
newObj[key] = deepClone(val[key]);
}
return newObj;
}
4、对Set和Map进行处理
set和map也是对象,他们也被神奇的拷贝成了空对象,如下。
所以我们也要对这两个特殊的对象进行处理。如果判断是set或者map类型的话,就直接新建一个set或者map(注意set和map新建是可以传入可迭代对象的,而set和map本身就是可迭代对象,所以可以直接传入并且new出新的来)
function deepClone(val) {
if (!isObject(val)) {
return val;
}
if(typeof val === 'function') return val
//通过instanceof判断类型,并且构造函数是可以传入可迭代对象的
if(val instanceof Set) return new Set(val)
if(val instanceof Map) return new Map(val)
let newObj = Array.isArray(val) ? [] : {};
for (const key in val) {
newObj[key] = deepClone(val[key]);
}
return newObj;
}
5、对Symbol进行处理
Symbol有两个问题:
- 如果
Symbol作为对象某个属性的value的话,那么返回的对象和原对象的这个属性的Symbol是同一个,因为它的isObject()是false,直接返回了。 Symbol作为对象的key的时候不会被拷贝,因为key为Symbol类型的话是不会被for遍历到的
function deepClone(val) {
//如果值是symbol类型,就获取symbol的描述符重新拷贝一份
//这里有个注意点,需要吧这个isObject判断放在前面,因为symbol不是对象类型,会直接返回的
if(typeof val === 'symbol') return Symbol(val.description)
if (!isObject(val)) {
return val;
}
if(typeof val === 'function') return val
if(val instanceof Set) return new Set(val)
if(val instanceof Map) return new Map(val)
let newObj = Array.isArray(val) ? [] : {};
for (const key in val) {
newObj[key] = deepClone(val[key]);
}
//获取所有的symbol类型的属性,对这些属性值进行深拷贝
const symbolKeys = Object.getOwnPropertySymbols(val)
for(const key of symbolKeys){
newObj[key] = deepClone(val[key])
}
return newObj;
}
6、对循环引用进行处理
循环引用主要是会导致递归循环调用,进而导致栈溢出
所以我们可以将拷贝的对象保存起来,如果在下次取之前先判断一下之前有没有拷贝过这个对象,有的话就获取这个对象直接返回。这里我们用map来保存
//默认值map是一个空的Map,这样可以实现函数内部值的私有化
function deepClone(val,map = new Map()) {
if(typeof val === 'symbol') return Symbol(val.description)
if (!isObject(val)) {
return val;
}
if(typeof val === 'function') return val
if(val instanceof Set) return new Set(val)
if(val instanceof Map) return new Map(val)
//如果前面有保存过拷贝的内容的话,就直接返回
if(map.has(val)){
return map.get(val)
}
let newObj = Array.isArray(val) ? [] : {};
//在map中保存一份拷贝的内容
map.set(val,newObj)
for (const key in val) {
newObj[key] = deepClone(val[key],map);
}
const symbolKeys = Object.getOwnPropertySymbols(val)
for(const key of symbolKeys){
newObj[key] = deepClone(val[key],map)
}
return newObj;
}
现在一个自己实现的深拷贝函数就大功告成啦,感谢观看。