# js如何实现深拷贝

js中实现深拷贝,主要包括下面几种方法:

  1. 借助JSON的stringify与parse方法,对一个对象进行序列化与反序列化。
  2. 递归拷贝一个对象的属性与值,到一个新的对象里。其中在拷贝值的时候,需要考虑这个值是普通类型,还是引用类型。

# JSON.stringify

使用JSON.stringifyJSON.parse来拷贝一个对象时,有如下问题:

  1. 它会丢失值为functionundefinedSymbol不可枚举类型的值;
  2. dateRegExp等类型的值转换错误:
    1. 其中Date类型的对象,默认转为了日期字符串。正则类型的对象,转成了空对象;
    2. new Number(1),new Boolean(false)等对象,错误的转成了相应的字符串、布尔值;
  3. 不能解决循环引用的对象(抛错);

看如下代码:

var source = {
    a1: undefined,
    a2: null,
    fn: function () {}, // 函数
    reg: /^path$/, // 正则
    date: new Date(), // 日期
    name: "jack",
    a3: 123,
    num2: new Number(2), // 被转成了数字2
    book: { title: "You Don't Know JS", price: "45" }
};
//  循环引用
//source.ref = source;

console.log(source, JSON.parse(JSON.stringify(souce)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

image.png如果对象中存在循环引用,则直接报错:

image.png

所以,对于普通对象而言,我们直接使用这种方式是没有问题的。一旦对象中出现了上述的值,则该方法不可行。

# 递归拷贝(循环拷贝)

既然JSON.stringify无法满足我们的要求,我们可以换一种思路。对一个对象进行递归遍历,分别将值拷贝到新的对象上,就可以达到我们深拷贝的要求,这里要注意以下几点:

在遍历对象的key时,有2种方式,一种是深度优先(DFS),一种是广度优先(BFS)。关于二者的区别,大家可以自行查阅。

拷贝之前,先定义一些基础工具方法:

const mapType = '[object Map]';
const setType = '[object Set]';
const arrayType = '[object Array]';
const objectType = '[object Object]';
const argsType = '[object Arguments]';
// 可遍历对象
const itTypes = [mapType, setType, arrayType, objectType, argsType];

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

function getType(obj) {
    return Object.prototype.toString.call(obj);
}
// 不可遍历对象
function cloneOtherType(target, type) {
     // 特殊对象: Date, RegExp, 以及"new Number(1)"这种通过构造函数创建的对象类型
    if (typeof target === "function") {
        // 函数, 不做处理直接返回(第三方库都没做处理), 或者返回一个新函数, 内部调用原来的函数
        // let fn = function () {
        //     target.apply(this, arguments);
        // }
        // return fn;
        return target;
    }else if (type == '[object RegExp]') {  // RegExp
        return new RegExp(target); 
    }else if (type == '[object Date]') {  // Date
        return new Date(target); 
    } else { // new Number(1), new String('jack')这种特殊的对象类型
        // return new target.constructor(target);
        // 等效于上面的写法
        return Object(Object.prototype.valueOf.call(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
32
33
34
35
36

# 深度优先DFS

// DFS
function deepClone_DFS(target, weakMap = new WeakMap()) {
    // 原始类型:symbol, string、number、boolean、null、undefined
    if (!isObject(target)) return target;
    let cloneTarget;
    const type = getType(target);
    // 可遍历对象:Map、Set、Object、Array、Arguments
    if (itTypes.includes(type)) {
        cloneTarget = new target.constructor();
    } else {
        // 非可遍历对象:Date, RegExp, new Number(100), new String('jack'), 以及function等
        return cloneOtherType(target, type);
    }
    if (weakMap.has(target)) return weakMap.get(target); // 处理循环引用值
    weakMap.set(target, cloneTarget);

    // 可遍历对象:Map、Set、Object、Array、Arguments
    // Map, Set: 迭代克隆
    if (type == setType) { // Set
        target.forEach(value => {
            cloneTarget.add(deepClone_DFS(value, weakMap)) });
        return cloneTarget;
    }
    if (type == mapType) { // Map
        target.forEach((value, key) => {
            cloneTarget.set(key, deepClone_DFS(value,weakMap));
        });
        return cloneTarget;
    }
    // Object, Array, Arguments: 遍历克隆
    // let keys = Object.keys(target);
    let keys = Reflect.ownKeys(target); // 包括可枚举、不可枚举、Symbol属性
    // let keys = Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));

    for (let key of keys) { // for of
        cloneTarget[key] = deepClone_DFS(target[key], weakMap);
    }
    return cloneTarget;
}
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

如果对象的层级够深时,使用深度优先的策略,可能会出现爆栈的情况(执行栈的大小是有限制的,chrome浏览器大概在层级深度为6000左右出现爆栈的情况),此时,我们可以考虑使用广度优先的策略。

由对象层级深而导致深拷贝爆栈的情况,在真实开发中,基本不会出现。如果真出现了,说明这个对象的设计是不合理的。

# 广度优先BFS

利用一个数组loopList,在循环对象的key时,不停向里面添加新的对象,直到整个loopList循环完成。无论层级多深,都不会有爆栈的情况。

var a = {
    a1: 1,
    a2: {
        b1: 1, b2: {  c1: 1  }
    }
};

function clone_BFS(target) {
    let root = {};
    const loopList = [
        { parent: root,  key: undefined,  data: target }
    ];
    while(loopList.length) {
        let node = loopList.pop();
        let { parent, key, data } = node;
        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if(typeof key != 'undefined') {
            res = parent[key] = {};
        }
        for(let k in data) {
            if(data.hasOwnProperty(k)) {
                if(typeof data[k] === 'object') {
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k]
                    });
                }else {
                    res[k] = data[k];
                }
            }
        }
    }
    return root;
}
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

# 参考