原文再续,书接上一回(【JavaScript笔记06】赋值、浅拷贝、深拷贝),在这篇文章实现一下深拷贝的方法。
首先来个最基本的拷贝,创建一个新对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上即可,但是面对多层的对象就不行了。
let obj1 = {
name:'jjy',
age:23,
child:{
id:4
}
}
function t1(source){
let cloneTarget = {}
for(const key in source){
cloneTarget[key] = source[key]
}
return cloneTarget
}
let obj2 = t1(obj1)
obj2.age = 30
obj2.child.id = 6
console.log(obj1) // { name: 'jjy', age: 23, child: { id: 6 } }
console.log(obj2) // { name: 'jjy', age: 30, child: { id: 6 } }
修改一下,利用递归:
- 遇到基本类型,无需递归,直接返回。
- 遇到对象,创建一个新的对象,递归进入进行遍历拷贝的操作。
但是遇到数组就不行了,不过也容易解决,加一个判断即可
let obj1 = {
name:'jjy',
age:23,
child:{
id:4
},
arr:[1,2,3,4]
}
function t2(source){
if(typeof source === 'object'){
let cloneTarget = {}
for(let key in source){
cloneTarget[key] = t2(source[key])
}
return cloneTarget
}else return source
}
let obj2 = t2(obj1)
obj2.age = 30
obj2.child.id = 100
obj2.arr[1] = 60
console.log(obj1) // { name: 'jjy', age: 23, child: { id: 4 }, arr: [ 1, 2, 3, 4 ] }
console.log(obj2)
/* {
name: 'jjy',
age: 30,
child: { id: 100 },
arr: { '0': 1, '1': 60, '2': 3, '3': 4 }
} */
修改后适配数组:
function t3(source) {
if (typeof source === 'object') {
let cloneTarget = Array.isArray(source) ? [] : {}
for (const key in source) {
cloneTarget[key] = t3(source[key])
}
return cloneTarget
} else return source
}
let obj2 = t3(obj1)
obj2.age = 30
obj2.child.id = 100
obj2.arr[1] = 60
console.log(obj1) // { name: 'jjy', age: 23, child: { id: 4 }, arr: [ 1, 2, 3, 4 ] }
console.log(obj2) // { name: 'jjy', age: 30, child: { id: 100 }, arr: [ 1, 60, 3, 4 ] }
由于使用到了递归,所以可能会出现循环引用的问题,当对象的属性间接或直接地引用了自身,递归进入死循环就会导致内存溢出,如下:
obj1.target = obj1
let obj2 = t3(obj1)
console.log(obj2)
针对循环引用,这里需要开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象。如果有直接返回,如果没有的话继续拷贝。
let obj1 = {
name: 'jjy',
age: 23,
}
function t4(source, map = new Map()) {
if (typeof source === 'object') {
let cloneTarget = Array.isArray(source) ? [] : {}
if (map.get(source)) {
return map.get(source)
}
map.set(source, cloneTarget)
for (const key in source) {
cloneTarget[key] = t4(source[key],map)
}
return cloneTarget
} else {
return source
}
}
obj1.target = obj1
let obj2 = t4(obj1)
console.log(obj2) // <ref *1> { name: 'jjy', age: 23, target: [Circular *1] }
这里重点注意的是cloneTarget
是对象,是引用类型,所以被map.set之后,后续的更改都会改变在map里面的cloneTarget的内容,所以第三次循环进入递归的时候,return map.get(source)
返回的是目前的cloneTarget的值{ name: 'jjy', age: 23}
,因为在第一、二次循环的时候改变了cloneTarget,map里面的cloneTarget也随之改变。返回之后,cloneTarget[target]
看着像是只有{ name: 'jjy', age: 23}
,但刚才说了cloneTarget
是引用类型,在某个位置的内容的改动会体现在其他任何位置,所以在map里面的cloneTarget的{ name: 'jjy', age: 23}
这个地方也会多了一个target属性,而这个target属性又会有cloneTarget本身,从而自动形成了一个循环引用,而且也不会有递归产生的内存溢出。
(画给自己看的图,请无视~)
基本上到这里一个可用的深拷贝就写完了,但还有可以优化的地方,这个后续再继续学习。