# 对深拷贝的研究

# 深拷贝和浅拷贝的定义

深拷贝:拷贝实例;浅拷贝:拷贝引用(原对象)。

# 说深拷贝和浅拷贝之前,我先去了解了下高程书上的 JavaScript 的变量类型

基本类型:undefined、null、Boolean、number、string。变量直接按指存放在栈区内,可以直接访问,所以我们平时把字符串、数字的值赋值给新变量,相当于把值完全复制过去,新变量的改变不会影响旧变量。

引用类型:存放在堆区的对象,变量在栈区中保存的是一个指针地址。

例子

   let a = 123;
   let b = a;
   b = 456;
   console.log(a);//123
   console.log(b);//456
1
2
3
4
5

深拷贝和浅拷贝图解

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

话不多说,浅拷贝就不再多说,下面我们直入正题:

# 乞丐版

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());
1

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、拷贝正则、拷贝 Date 类型、循环引用等情况不行。

# 基础版本 1

//基础版本

function clone(target) {
    let cloneTarget = {};
    for (let i in target) {
        cloneTarget[i] = target[i]
    }
    return cloneTarget;
}
1
2
3
4
5
6
7
8
9

# 基础版本 2

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

如果是原始类型,无需继续拷贝,直接返回

如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝


function clone(target) {
    if (typeof target === "object") {
        let cloneTarget = {};
        for (let i in target) {
            cloneTarget[i] = clone(target[i])
        }
        return cloneTarget;
    } else {
        return target;
    }
}
let obj = {
    a: 1,
    b: {
        c: {
            d: function() {
                console.log(1)
            }
        }
    }
}
let obj1 = clone(obj)
obj1['b']['c']['d'] = 'ee'
console.log(obj1)
console.log(obj)
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

# 基础版本 3

在上面的版本中,我们的初始化结果只考虑了普通的 object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了:

function clone(target) {
            if (typeof target === "object") {
                let cloneTarget = Array.isArray(target) ? [] : {}
                for (let i in target) {
                    cloneTarget[i] = clone(target[i])
                }
                return cloneTarget;
            } else {
                return target
            }
        }
    let obj = {
        a: 1,
        b: {
            c: [1]
        }
    }
    let obj1 = clone(obj);
    obj1['b']['c'].push(2)
    console.log(obj)
    console.log(obj1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 基础版本 4

循环引用,一般我们很少会遇到这种情况,首先我们执行一个测试用例

function clone(target) {
    if (typeof target === "object") {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (let i in target) {
            cloneTarget[i] = clone(target[i])
        }
        return cloneTarget
    } else {
        return target;
    }
}

let obj = {
    a: 1,
    b: {
        c: [1]
    }
}
obj.obj = obj;
let obj1 = clone(obj);
obj1['b']['c'].push(2)
console.log(obj)
console.log(obj1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

可以看到下面的结果:

很明显,因为递归进入死循环导致栈内存溢出了。

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

循环引用是一个不常见的现象,但是像 react 的 dome 节点的话很可能会出现循环引用,例如一个 span 节点中会出现一个 span 节点,这个很可能会相互引用。

那么怎么解决呢?来看看这段代码

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

1.检查 map 中有无克隆过的对象

2.有 - 直接返回

3.没有 - 将当前对象作为 key,克隆对象作为 value 进行存储

4.继续克隆

function clone(target, map = new Map()) {
    if (typeof target === "object") {
        let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget)
        for (let i in target) {
            cloneTarget[i] = clone(target[i], map)
        }
        return cloneTarget
    } else {
        return target;
    }
}

let obj = {
    a: 1,
    b: {
        c: [1]
    }
}
obj.obj = obj;
let obj1 = clone(obj);
obj1['b']['c'].push(2)
console.log(obj)
console.log(obj1)
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

再来执行上面的测试用例:

可以看到,执行没有报错,且 target 属性,变为了一个 Circular 类型,即循环应用的意思。

循环引用有时候会遇到这种情况,所以我们应该考虑到这种情况。

接下来,我们可以使用, WeakMap 提代 Map 来使代码达到画龙点睛的作用


function clone(target, map = new WeakMap()) {
    if (typeof target === "object") {
         let cloneTarget = Array.isArray(target) ? [] : {};
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget);
        for (let i in target) {
            cloneTarget[i] = clone(target[i], map)
        }
        return cloneTarget;
    } else {
        return target;
    }
}

let obj = {
    a: 1,
    b: {
        c: []
    }
}
obj.obj = obj;
let obj1 = clone(obj);

obj1['b']['c'] = function() {
    console.log(1)
}

console.log(obj)

console.log(obj1)

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
33
34

# 为什么要这样做呢?,先来看看 WeakMap 的作用

# WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的

WeakMap 是 map 的变体,二者的多数外部行为特性都是一样的,区别在于内部内存分配 (特别是其 GC)的工作方式。

WeakMap 没有 size 属性或 clear() 方法,也不会暴露任何键、值或项目上的迭代器。所以即使你解除了对 x 的引用,它将会因 GC 时这个条目被从 m 中移除,也没有办法确定这 一事实。所以你就相信 JavaScript 所声明的吧! 和 Map 一样,通过 WeakMap 可以把信息与一个对象软关联起来。而在对这个对象没有完 全控制权的时候,这个功能特别有用,比如 DOM 元素。如果作为映射键的对象可以被删除,并支持垃圾回收,那么 WeakMap 就更是合适的选择了。

来看下面的一个实例

var m = new WeakMap();
var x = {
        id: 1
    },
    y = {
        id: 2
    },
    z = {
        id: 3
    },
    w = {
        id: 4
    };
m.set(x, y);
x = null; // { id: 1 } 可GC
y = null; // { id: 2 } 可GC 只因 { id: 1 } 可GC
m.set(z, w);

w = null // { id: 4 } 不可GC

console.log(m)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 对 for in 循环的优化

在上面的代码中,我们遍历数组和对象都使用了 forin 这种方式,实际上 forin 在遍历时效率是非常低的,

通过测试三种循环 for、while、forin 的执行效率,while 的效率是最好的,所以,我们可以想办法把 forin 遍历改变为 while 遍历。

我们先使用 while 来实现一个通用的 forEach 遍历, iteratee 是遍历的回掉函数,他可以接收每次遍历的 value 和 index 两个参数:

function forEach(target, iteratee) {
    let index = -1;
    const len = target.length;
    while (++index < len) {
        iteratee(target[index], index)
    }
    return target;
}

function clone(target, map = new WeakMap()) {
    if (typeof target === "object") {
        let cloneTarget = Array.isArray(target) ? [] : {}
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget);
        // for (let i in target) {
        //     cloneTarget[i] = clone(target[i], map)
        // }
        let keys = Array.isArray(target) ? undefined : Object.keys(target)
        forEach(keys || target, (v, i) => {
            if (keys) {
                i = v;
            }
            cloneTarget[i] = clone(target[i], map)
        })
        return cloneTarget;
    } else {
        return target;
    }
}
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

# 其他数据类型

# 合理判断是否为引用类型

首先先判段是否为引用类型,在这判断时注意 ⚠️ null 与 function 两种特殊类型

function isProtoType(target) {
    let type = typeof target;
    return target !== null && (type === "object" || type === "function");
}
1
2
3
4

# 获取数据类型

我们可以使用 toString 来获取准确的引用类型:

每一个引用类型都有 toString 方法,默认情况下, toString()方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString()返回 "[object type]",其中 type 是对象的类型。

注意 ⚠️,上面提到了如果此方法在自定义对象中未被覆盖, toString 才会达到预想的效果,事实上,大部分引用类型比如 Array、Date、RegExp 等都重写了 toString 方法

function getType(target) {
    return Object.prototype.toString.call(target).slice(8, -1);
}
1
2
3

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = 'Map';

const setTag = 'Set';

const arrayTag = 'Array';

const objectTag = 'Object';



const boolTag = 'Boolean';

const dateTag = 'Date';

const errorTag = 'Error';

const numberTag = 'Number';

const regexpTag = 'RegExp';

const stringTag = 'String';

const symbolTag = 'Symbol';



const bufferTag = 'Uint8Array';
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

在上面的集中类型中,我们简单将他们分为两类:

可以继续遍历的类型

不可以继续遍历的类型

我们分别为它们做不同的拷贝。

# 可继续遍历的类型

上面我们已经考虑的 object、 array 都属于可以继续遍历的类型,因为它们内存都还可以存储其他数据类型的数据,另外还有 Map, Set 等都是可以继续遍历的类型,这里我们只考虑这四种;

有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的 []和 {},我们可以通过拿到 constructor 的方式来通用的获取。

例如 let consttarget={}就是 let consttarget=new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,类似于装箱,所以它可以保留对象原型上的数据,如果直接使用普通的 {},那么原型必然是丢失了的。

下面,我们改写 clone 函数,对可继续遍历的数据类型进行处理:

function foreach(target, callback) {
    let index = -1,
        len = target.length;
    while (++index < len) {
        callback(target[index], index)
    }
    return target;
}

function isProtoType(target) {
    let type = typeof target;
    return target !== null && (type === "object" || type === "function");
}

function getType(target) {
    return Object.prototype.toString.call(target).slice(8, -1);
}

const mapTag = 'Map';

const setTag = 'Set';

const arrayTag = 'Array';

const objectTag = 'Object';



const boolTag = 'Boolean';

const dateTag = 'Date';

const errorTag = 'Error';

const numberTag = 'Number';

const regexpTag = 'RegExp';

const stringTag = 'String';

const symbolTag = 'Symbol';



const bufferTag = 'Uint8Array';

function getInit(target) {
    let ClassNames = target.constructor;

    return new ClassNames();
}
//好了一切准备别就绪,可继续遍历的类型 考虑类型 Object Array Map Set

const deepTag = [mapTag, setTag, objectTag, arrayTag];

function clone(target, map = new WeakMap()) {
    //克隆原始类型
    if (!isProtoType(target)) {
        return target;
    }
    //初始化类型
    const type = getType(target);
    let cloneTarget;

    if (deepTag.includes(type)) {

        cloneTarget = getInit(target);
    }

    //防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);
    //如果是map拷贝

    if (type === mapTag) {
        target.forEach((v, key) => {
            cloneTarget.set(key, clone(v, map))
        })
        return cloneTarget;
    }

    //拷贝set

    if (type === setTag) {
        target.forEach(v => {
            cloneTarget.add(clone(v, map))
        })
        return cloneTarget;
    }

    //拷贝数组与对象

    const keys = type === arrayTag ? undefined : Object.keys(target);
    foreach(keys || target, (v, index) => {
        if (keys) {
            index = v;
        }
        cloneTarget[index] = clone(target[index], map)
    })
    return cloneTarget;
}
const map = new Map();

map.set('key', 'value');

map.set('name', 'wendaoshuai')



const set = new Set();

set.add('11').add('liushuai')

const target = {

    field1: 1,

    field2: undefined,

    field3: {

        child: 'child'

    },

    field4: [
        2,
        4,
        8
    ],
    empty: null,
    map,
    set
};
target.target = target;
const target1 = clone(target)
target1.a = "a"
console.log('🍎', target)
console.log('🍌', target1)
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141