ES5-ES6贯穿对象深浅拷贝

1,173 阅读5分钟

前言

先划重点,深浅拷贝是一道面试必考题。 在JavaScript中有不同的方法来拷贝一个对象,如果你不是很熟悉的话,在拷贝对象时就容易犯一些错误,那么我们应该如何正确的来拷贝一个对象呢?
本文我将会给大家贯穿深拷贝的问题,由浅入深,坏坏相扣,共4种拷贝方式。
阅读完本篇文章,我们应该明白:

  1. 什么是深浅拷贝,他们之间有什么区别,和普通赋值又有什么不同?
  2. 深浅拷贝对象又有哪些方法,他们之间又有什么缺点?

什么是深拷贝,什么是浅拷贝?

深浅拷贝其实就是都是针对的引用数据类型,JS数据类型中分为基本数据类型(值类型)引用数据类型

  • 基本数据类型:undefined null Number String Boolean Symbol
  • 引用数据类型:Object -> ...Array Set Map WeakMap...

基本数据类型进行复制操作的话是对值进行了拷贝操作,是一个新的值;引用数据类型进行复制操作其实是复制的堆内存中的地址,两个变量是指向同一个地址,改变一个会影响另一个。

    // 基本数据类型
    var a = 1
    var b = a
    a = 2
    console.log(a, b) // 变量a和b是不同的数据

    // 引用数据类型
    var obj = { name: '张三' }
    var obj2 = obj
    obj2.name = '李四'
    console.log(obj, obj2) // 变量obj和obj2指向同一个地址 是同一份数据

image.png

浅拷贝

既然知道了两个变量指向同一个地址,那么我们怎么切断这个联系呢?我们可以使用浅拷贝来解决这个问题,划重点:浅拷贝只会拷贝引用类型的第一层数据,适合只有一层的数据的时候

    // 浅拷贝 其实就是遍历对象属性
    // 方法一
    var obj3 = {
        info: {
            name: '张三'
        },
        age: 23
    }

    var obj4 = {}
    for(var key in obj3) {
        obj4[key] = obj3[key]
    }

    obj4.age = 24 // 改变obj4的age不会影响obj3 因为浅拷贝只拷贝了第一层数据
    obj4.info.sex = '男' // 给obj4的info增加了一个属性sex,导致obj3也增加了

    console.log(obj3, obj4) 
    // 方法二
    // Object.assign
    // 对象合并,将源对象的所有可枚举值复制到目标对象上,如果源对象里面的值是一个对象,那么只会复制该对象的引用地址
    var obj5 = Object.assign({}, obj3)
    obj5.age = 24 // 改变obj4的age不会影响obj3 因为浅拷贝只拷贝了第一层数据
    obj5.info.sex = '男' // 给obj4的info增加了一个属性sex,导致obj3也增加了

    console.log(obj3, obj5) 

深拷贝

其实就是浅拷贝和递归的结合版

    // 方法一
    // 最简单的深拷贝
    // JSON.parse(JSON.string)
    // 通过先对对象进行JSON字符串化,再通过parse解析出来
    // 这个方法有一些弊端
    var obj6 = {
    info: {
        name: '张三'
    },
    age: 23,
    a: undefined,
    b: null,
    e: function() {},
    d: new Set([1, 3, 4]),
    f: new Map([{name: '王五'}]),
    g: Symbol('name')
    }

    var obj7 = JSON.parse(JSON.stringify(obj6))
    obj7.age = 24
    obj7.info.sex = '男'
    /**
    * 通过打印我们可以看出
    * JSON字符串化后再解析出来的数据丢失了一些
    * undefined function Symbol丢失了
    * Set和Map变成了空对象
    * 因为在JSON序列化时会忽略undefined function Symbol,而Map/Set/WeakMap/WeakSet则会被序列化成可枚举的属性
    */
    console.log(obj6, obj7) 

    // 包含循环引用的对象会报错 对象之间相互引用 形成无限循环
    var data = {
     name: 'foo',
     child: null
    }

    data.child = data
    var data2 = JSON.stringify(JSON.parse(data)) // 报错
    console.log(data, data2)
    // 方法二 ES5
    // 遍历+递归
     function deepClone(origin, target) {
            var target = target || {},
                toStr = Object.prototype.toString,
                isArr = '[object Array]';

            for(var key in origin) {
                if(origin.hasOwnProperty(key)) {
                    if(typeof origin[key] === 'object' && origin[key] !== null) {
                        target[key] = toStr.call(origin[key]) === isArr ? [] : {}
                        deepClone(origin[key], target[key])
                    } else {
                        target[key] = origin[key]
                    }
                }
            }

            return target
         }
    // 方法三
    // 这个方法有一个弊端 
    // 当对象包含循环引用的对象时 形成循环就会报错
    function deepClone(origin) {
        // undefined 双等于 null 
        // typeof null = object
        if(origin == undefined || typeof origin !== 'object') {
            return origin
        }

        if(origin instanceof Date) {
            return new Date(origin)
        }

        if(origin instanceof RegExp) {
            return new RegExp(origin)
        }
        
        // constructpr指向构造函数 {} -> Object [] -> Array
        let target = new origin.constructor()

        for(let key in origin) {
            if(origin.hasOwnProperty(key)) {
                target[key] = deepClone(origin[key])
            }
        }

        return target
    }

    var obj8 = deepClone(obj3)
    obj8.info.age = 24
    console.log(obj8, obj3)
    var data = {
         name: 'foo',
         child: null
     }

     data.child = data
     var data2 = deepClone(data) // 报错 爆栈
     console.log(data, data2)

WeakMap

为了解决对象循环引用,导致的爆栈问题,我们需要来认识一个WeakMap;WeakMap对象是一组键/值对的集合,其中的键是弱引用的,且必须是对象类型,值可以是任意的
WeakMap的键不可枚举,当键所指的对象没有在其他地方引用时,将会被GC垃圾回收机制回收;
Map 是 ES6 中新增的数据结构,Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型,其中的键是强引用...

    let mapData = { name: '张三' }
    let mapData2 = { name: '李四' }
    const oBtnWeakMap = new WeakMap()
    const oBtnMap = new Map()
    oBtnWeakMap.set(mapData, mapData2)
    oBtnMap.set(mapData, mapData2)
    mapData = null 
    // mapData赋值为null后,Map里面的mapData2并没有被回收,还被牵着
    // 而WeakMap中的因为没有被外界使用,故而被GC垃圾回收机制回收了

浏览器中无法验证变量是否被回收,我们可以在node环境里面来验证下:

image.png 从图中可以看出,当给map增加了一个key键的1值的对象后,内存增大,将key赋值为null后,内存并没有得到释放;要想释放内存,需要先delete(key),然后再将key赋值null。

image.png 这个时候因为WeakMap时弱引用,当key为null时会自动被GC垃圾回收机制回收,省去了我们手动delete(key)步骤。

image.png

利用WeakMap解决深拷贝 对象循环引用爆栈问题
function deepClone(origin, hashMap = new WeakMap()) {
    if(origin == undefined || typeof origin !== 'object') {
        return origin
    }

    if(origin instanceof Date) {
        return new Date(origin)
    }

    if(origin instanceof RegExp) {
        return new RegExp(origin)
    }

    const hashKey = hashMap.get(origin)

    if(hashKey) {
        return hashKey
    }

    let target = new origin.constructor()

    hashMap.set(origin, target)

    for(let key in origin) {
        if(origin.hasOwnProperty(key)) {
            target[key] = deepClone(origin[key], hashMap)
        }
    }

    return target
}

var data = {
    name: 'foo',
    child: null
}

data.child = data
var data2 = deepClone(data)
console.log(data, data2)