深拷贝、浅拷贝与循环引用

对于javascript而言,想要实现对象的拷贝/复制,单纯的使用赋值语句是不全面的,对于简单数据(值类型)是没问题的,但是对于对象这种复杂数据类型就会有意想不到的问题。一般而言,深拷贝与浅拷贝只针对像Object,Array这样的复杂对象。浅拷贝只拷贝一层对象的属性,而深拷贝则递归拷贝了所有层级。

浅拷贝与深拷贝的区别

  • 浅拷贝:只会将目标对象的第一层的各个属性依次复制,不会进行递归复制。浅拷贝时,如果属性是基本类型值,拷贝的是基本类型的值,改变其中不一个对象不会改变另一个对象。如果属性是引用类型地址,并不会分配新的对地址,是将两个对象指向同一地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
  • 深拷贝:递归拷贝目标对象的所有属性。深拷贝会在堆内存开辟新的地址,两个对象对应两个不同的地址,修改其中一个不会改变另一个对象。

浅拷贝的实现方式

  • 数组的浅拷贝方式 arr.concat() [...arr] arr.slice()
  • for in - 遍历属性拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 用for in遍历的时候不支持对 Symbol属性 的处理
    let obj = {
    a: 100,
    b: [10, 20],
    c: {
    x: 10
    }
    }
    let obj2 = {}
    for (let key in obj) {
    if (!obj.hasOwnProperty(key)) break
    obj2[key] = obj[key]
    }
    // console.log(obj2) // {a: 100, b: [10, 20], c: { x:10 }}
    obj2.a = 200 // 修改对象属性为基本类型值 另一个对象不会改变 值类型拷贝 开辟新的栈
    obj2.b[1] = 50 // 修改对象属性为引用类型时 另一个对象会改变 浅拷贝时引用类型拷贝的是引用类型的指针
    console.log(obj2) // {a: 200, b: [10, 50], c: { x:10 }}
    console.log(obj) // {a: 100, b: [10, 50], c: { x:10 }}
  • 扩展运算符 ...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let obj = {
    a: 100,
    b: [10, 20],
    c: {
    x: 10
    }
    }
    let obj2 = {...obj}
    // console.log(obj2) // {a: 100, b: [10, 20], c: { x:10 }}
    obj2.a = 200 // 修改对象属性为基本类型值 另一个对象不会改变 值类型拷贝 开辟新的栈
    obj2.b[1] = 50 // 修改对象属性为引用类型时 另一个对象会改变 浅拷贝时引用类型拷贝的是引用类型的指针
    console.log(obj2) // {a: 200, b: [10, 50], c: { x:10 }}
    console.log(obj) // {a: 100, b: [10, 50], c: { x:10 }}
  • Object.assign()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let obj1 = {
    a: 100,
    b: [10, 20],
    c: {
    x: 10
    }
    }
    const obj2 = Object.assign({}, obj1)
    obj2.a = 200 // 改变对象基本类型值 原对象未改变
    obj2.c.x = 200 // 改变对象引用类型值 原对象也被改变
    console.log(obj2) // {a: 200, b: [10, 20], c: { x: 200 }}
    console.log(obj1) // {a: 100, b: [10, 20], c: { x: 200 }}

深拷贝的实现方式

  • JSON.parse(JSON.stringify(obj))
    存在弊端 属性值为 NaN undefined symbol 正则 函数 Date 会有问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // a. 属性值为正则表达式时 会将 正则转化为 {}
    // b. 属性值为任意函数, undefined, symbol时 在非数组中 出现丢失 会被忽略, 在数组中会被转化为null
    // c. 属性值为 new Date() 时 会转化为具体时间"2019-05-20T14:02:41.568Z"
    // d. 会抛弃对象原来的constructor,不管这个对象原来的构造函数是什么,深拷贝后都会变成Object
    // e. 无法处理对象中存在循环引用问题
    let obj = {
    a: 100,
    b: [10, undefined],
    c: {x: 10},
    d: /^\d+$/,
    e: function(){},
    f: new Date(),
    g: undefined,
    h: Symbol('tew')
    }
    const obj2 = JSON.parse(JSON.stringify(obj))
    console.log(obj2) // {a:100,b:[10,null],c:{x:10},d:{},f:"2019-05-20T14:02:41.568Z"]
  • 递归拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    let obj = {
    a: 100,
    b: [10, undefined],
    c: {x: 10},
    d: /^\d+$/,
    e: function(){},
    f: new Date(),
    g: undefined,
    h: Symbol('tew')
    }
    // 比较完善的递归拷贝 不考虑对象的循环引用
    function deepClone(obj){
    // 过滤特殊情况
    if (obj === null) return null
    if (typeof obj !== "object") return obj // 不是对象就不用拷贝
    if (obj instanceof RegExp) return new RegExp(obj)
    if (obj instanceof Date) return new Date(obj)
    // let newObj = {} 会使 数组变成对象{} 如 [10, undefined] 变成 {0:10,1:undefined}
    // 不直接创建空对象目的:克隆的结果和之前保持相同的所属类 如Array和Symbol
    // 防止抛弃对象原来的constructor 深拷贝后变成 Object
    let newObj = new obj.constructor;
    for (let key in obj) {
    // 保证 key 不是原型的属性
    if (obj.hasOwnProperty(key)) {
    newObj[key] = deepClone(obj[key]) // 将值进行递归调用
    }
    }
    console.log(JSON.stringify(newObj))
    return newObj;
    }
    deepClone(obj)
    // {a:100,b:[10,undefined],c:{x:10},d:/^\d+$/,e:f(),f:2019-05-20T14:02:41.568Z,g:undefined,h: Symbol(tew)}

对象存在循环引用使用递归深拷贝会出现栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = {
x: 1,
y: 2
}
obj.z = obj
function deepClone(obj){
// 过滤特殊情况
if (obj === null) return null
if (typeof obj !== "object") return obj
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
let newObj = new obj.constructor;
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
console.log(JSON.stringify(newObj))
return newObj;
}
deepClone(obj)
// Uncaught RangeError: Maximum call stack size exceeded

使用缓存解决循环引用避免无限套娃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const obj = {
x: 1,
[Symbol.toStringTag]: 'object'
}
obj.z = obj
function deepClone(obj, cache = new Set()){
// 过滤特殊情况
if (obj === null) return null
if (typeof obj !== "object") return obj
if (obj instanceof RegExp) return new RegExp(obj)
if (obj instanceof Date) return new Date(obj)
let newObj = new obj.constructor;

// 避免无限套娃
if (cache.has(obj)) return obj;
cache.add(obj);

// 处理for in 不能处理对象的symbol属性
let keys = [
...Object.keys(obj),
...Object.getOwnPropertySymbols(obj)
]
keys.map((item) => {
// 再次调用deepClone的时候把catch传递进去,保证每一次递归都是一个cache
newObj[item] = deepClone(obj[item], cache)
})
return newObj;
}
deepClone(obj)
-------------本文结束感谢您的阅读-------------
0%