原文再续,书接上一回(【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)

js es6 深拷贝 js深拷贝的实现方式_前端

针对循环引用,这里需要开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象。如果有直接返回,如果没有的话继续拷贝。

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本身,从而自动形成了一个循环引用,而且也不会有递归产生的内存溢出。

(画给自己看的图,请无视~)

js es6 深拷贝 js深拷贝的实现方式_开发语言_02

基本上到这里一个可用的深拷贝就写完了,但还有可以优化的地方,这个后续再继续学习。