# 👉 深浅拷贝的实现

# 直接赋值

当目标对象对赋值对象通过直接赋值的方式进行拷贝,这种情况下,对于原始类型就是直接赋值,而对于引用类型仅能拷贝了地址,改变一方属性值,由于是同一个引用,会导致同个地址对应的属性值都发生变化。

let a = {
    age: 1
};
let b = a;
a.age = 2;
console.log(b.age); // 2

# 浅拷贝和深拷贝的区别

# 浅拷贝

浅拷贝会将对象的各个属性进行依次复制,但并不会进行递归复制,即只会赋值目标对象的第一层属性。

对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;
而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」。

常见的浅拷贝:Object.assign

Object.assign 会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的依然是属性的引用,因此只是浅拷贝,并不是深拷贝。

# 深拷贝

深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。

常用的深拷贝:JSON.parse(JSON.stringify())

JSON.parse(JSON.stringify())存在的问题:

1)对于 Date 类型,会将时间类型序列化成字符串;
2)对于 Error、RegExp 对象只会拷贝得一个空对象;
3)对于 NaN、Infinity、-Infinity,会变成 null;
4)会丢失构造函数的属性方法;
5)无法解决循环引用问题;

# 浅拷贝的简单实现

function shadowClone(target) {
    if (typeof target === "object" && target !== null) {
        const newTarget = target instanceof Array ? [] : {};
        for (let key in target) {
            // 避免把继承的属性也一并拷贝
            if (target.hasOwnProperty(key)) {
                newTarget[key] = target[key];
            }
        }
        return newTarget;
    } else {
        return target;
    }
}

const arr1 = [2, 3, 5, { name: "arr1" }];
const arr2 = shadowClone(arr1);
arr2[3].name = "arr2";
console.log("arr1:", arr1);
console.log("arr2:", arr2);

// arr1:[2, 3, 5, {name: 'arr2'}]
// arr2:[2, 3, 5, {name: 'arr2'}]

# 深拷贝的简单实现

接下来是比较头大的深拷贝,深拷贝要在浅拷贝的基础上进行更深层的递归拷贝, 以及对各种特殊数据类型和边界情况的处理。

这里参考了掘金的一篇文章:如何写出一个惊艳面试官的深拷贝? (opens new window)
同时,也推荐阅读关于lodash 的深拷贝源码分析 (opens new window)相关的文章,进行更深入理解。

// 判断是否为引用类型
function isObject(source) {
    return (
        (typeof source === "object" || typeof source === "function") &&
        source !== null
    );
}

// 获取准确的引用类型
function getType(source) {
    return Object.prototype.toString.call(source);

    // example:
    // Object.prototype.toString.call(true)          ->  [object Boolean]
    // Object.prototype.toString.call(123)           ->  [object Number]
    // Object.prototype.toString.call(null)          ->  [object Null]
    // Object.prototype.toString.call(JSON)          ->  [object JSON]
    // Object.prototype.toString.call(function(){})  ->  [object Function]
    // Object.prototype.toString.call(new Set())  ->  [object Set]
}

// 支持遍历进行深拷贝的引用类型
const supportTraverse = {
    "[object Array]": true,
    "[object Object]": true,
    "[object Map]": true,
    "[object Set]": true
};

// 对不支持遍历克隆的引用类型处理
// 多为包装类型对象:Boolean、Number、String、Date、Error、Symbol 直接用构造函数和原始数据创建一个新对象
function handleUnTraverse(target, type) {
    const ctor = target.constructor;
    switch (type) {
        case "[object Boolean]":
            return new Object(Boolean.prototype.valueOf.call(target));

        case "[object String]":
            return new Object(String.prototype.valueOf.call(target));

        case "[object Number]":
            return new Object(Number.prototype.valueOf.call(target));

        case "[object Symbol]":
            return new Object(Symbol.prototype.valueOf.call(target));

        case "[object Error]":
        case "[object Date]":
            return new ctor(target);

        case "[object Function]":
            // 函数需要考虑到的情况比较多也比较麻烦,比如克隆以后可能会涉及函数作用域、上下文变化等问题
            // 此处直接返回函数的引用
            return target;

        default:
            return new ctor(target);
    }
}

function deepClone(target, map = new WeakMap()) {
    // 克隆原始类型数据
    if (!isObject(target)) {
        return target;
    }

    // 根据目标对象是否能遍历进行初始化
    let targetClone;

    const type = getType(target);

    if (!supportTraverse[type]) {
        // 对不支持递归遍历的对象类型初始化并克隆
        return handleUnTraverse(target, type);
    } else {
        // 新建空对象,保证原型不丢失
        const ctor = target.constructor;
        targetClone = new ctor();
    }

    // 解决循环引用
    // map 用于标识对象是否已被拷贝过
    if (map.get(target)) {
        // 已拷贝过的直接返回,否则进行逐个属性的拷贝并标记已拷贝
        return target;
    }

    // 标记已拷贝
    map.set(target, true);

    // Map 和 Set 的添加元素方法不同,分别为 set 和 add
    if (type === "[object Map]") {
        target.forEach((value, key) => {
            targetClone.set(key, deepClone(value, map));
        });

        return targetClone;
    }

    if (type === "[object Set]") {
        target.forEach(value => {
            targetClone.add(deepClone(value, map));
        });

        return targetClone;
    }

    // 克隆对象和数组
    for (let propName in target) {
        if (target.hasOwnProperty(propName)) {
            targetClone[propName] = deepClone(target[propName], map);
        }
    }

    return targetClone;
}

// test
const map = new Map();
map.set("key", "value");
map.set("ConardLi", "code秘密花园");

const set = new Set();
set.add("ConardLi");
set.add("code秘密花园");

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: "child"
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    // reg: /\d+/,
    error: new Error(),
    func1: (a, b) => {
        return a + b;
    },
    func2: function() {
        console.log("in func2", this, this.field3);
        // console.log('code秘密花园');
    }
};

const result = deepClone(target);

console.log(target);
target.func2();

console.log(result);
result.func2();

console.log("-----change result field3");
result.field3.child = "result filed3 child";

console.log("target:", target);
target.func2();

console.log("result:", result);
result.func2();