# 浅拷贝与深拷贝

# 什么时候需要拷贝

赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响

对基本类型进行赋值操作,两个变量互不影响。

let a = "Libai";
let b = a;
console.log(b);     // Libai

a = "xiaobai";
console.log(a);     // xiaobai
console.log(b);     // Libai

对引用类型进行赋址操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。

let a = [1, 2, 3]
let b = a;
b[0] = 100;

console.log(a)  // [100, 2, 3]
console.log(b)  // [100, 2, 3]

通常在开发中并不希望改变变量 b 之后会影响到变量 a,这时就需要用到浅拷贝和深拷贝。

# 浅拷贝(Shallow Copy)

let a = [1, 2, 3];
let b = a.slice();
b[0] = 100;

console.log(a) // [1, 2, 3]
console.log(b) // [100, 2, 3]

当修改b的时候,a的值并不改变。什么原因? 因为这里ba浅拷贝后的结果,ba现在引用的已经不是同一块空间啦!

浅拷贝有一个限制性的问题, 就是只会拷贝嵌套对象的第一层, 只能拷贝一层对象。

let a = [1, 2, 3, {aa: 22}];
let b = a.slice();
b[3].aa = 100;
b[0] = 10086;

console.log(a) // [1, 2, 3, {aa: 100}]
console.log(b) // [10086, 2, 3, {aa: 100}]

如果想拷贝多层的话, 这时候就需要深拷贝了, 不过会在后面讲解.

先来列一下实现浅拷贝的方法吧~~

# - 手动实现

老规矩, 循环解法~

const shallowClone = (target) => {
    if (typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target) ? []: {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = target[prop];
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}

# - Object.assign()

Object.assign()方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

const obj1 = {x: 1, y: 2};
const obj2 = Object.assign({}, obj1);

obj2.x = 2;
console.log(obj1) //{x: 1, y: 2} //原对象未改变
console.log(obj2) //{x: 2, y: 2}

const obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
const obj2 = Object.assign({}, obj1);

obj2.y.m = 2;
console.log(obj1) //{x: 1, y: {m: 2}} 原对象也被改变
console.log(obj2) //{x: 2, y: {m: 2}}

# - Array.concat()

针对数组能实现类似效果的还有slice()和Array.from()等, 开头的例子

const arr = [1,2,3,4,[5,6]];
const copy = arr.concat();
   
copy[0] = 2; 
console.log(arr) // [1,2,3,4,[5,6]];

// slice & 扩展运算符
const copy = arr.slice();
const copy = [...arr];

# 深拷贝(Deep Copy)

划重点, 面试必备, 学不学(掉不掉头发)自己看着办~~

# 1. 简单实现

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

const isObject = (target) => typeof target === 'object' && target !== null;

// 兼容数组和对象
const deepClone = (target) => {
    if (isObject(target)) {
        const newObj = Array.isArray(target) ? []: {};
        for (let key in target) {
            if (target.hasOwnProperty(key)) {
                if (isObject(target[key])) {
                    newObj[key] = deepClone(target[key])
                } else {
                    newObj[key] = target[key]
                }
            }
        }
        return newObj;
    } else {
        return target;
    }
}

typeof null //"object"
typeof {} //"object"
typeof [] //"object"
typeof function foo(){} //"function" (特殊情况)

# 2. 循环引用

let obj = {val : 100};
obj.target = obj;

deepClone(obj); // Uncaught RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

# 1. 使用哈希表

解决方案很简单,其实就是循环检测,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。

const deepClone = (target, hash = new WeakMap()) => {
    if(hash.has(target)){
        return hash.get(target) // 查哈希表
    }
    if (isObject(target)) {
        const newObj = Array.isArray(target) ? []: {};
        hash.set(target, newObj)    // 哈希表设值
        for (let key in target) {
            if (target.hasOwnProperty(key)) {
                if(isObject(target[key])){
                    newObj[key] = deepClone(target[key], hash); // 传入哈希表
                } else {
                    newObj[key] = target[key]
                }
            }
        }
        return newObj;
    } else {
        return target;
    }
}

测试一下,看看效果如何。

var a = {
    name: "Libai",
    book: {
        title: "You Don't Know JS",
        price: "45"
    },
    a1: undefined,
    a2: null,
    a3: 123
}
a.target = a;
console.log(deepClone(a))
// {
// 	name: "Libai",
// 	a1: undefined,
//	a2: null,
// 	a3: 123,
// 	book: {title: "You Don't Know JS", price: "45"},
// 	circleRef: {name: "Libai", book: {…}, a1: undefined, a2: null, a3: 123, …}
// }

完美!

# 2. 使用数组

上卖弄使用了ES6中的 WeakMap 来处理,那在 ES5 下应该如何处理呢?

也很简单,使用数组来处理就好啦,代码如下。

const deepClone = (target, uniqueList = []) => {
    if (isObject(target)) {

        const newObj = Array.isArray(target) ? []: {};

        // 数据已经存在,返回保存的数据
        const uniqueData = find(uniqueList, target)
        if (uniqueData) {
            return uniqueData.target
        }

        // 数据不存在, 保存源数据
        uniqueList.push({
            target,
            newObj
        })

        for (let key in target) {
            if (target.hasOwnProperty(key)) {
                if(isObject(target[key])){
                    newObj[key] = deepClone(target[key], uniqueList); // 传入哈希表
                } else {
                    newObj[key] = target[key]
                }
            }
        }
        return newObj;
    } else {
        return target;
    }
}

// 用于查找
function find(arr, item){
    for(let i = 0; i < arr.length; i++){
        if(arr[i].target === item){
            return arr[i]
        }
    }
    return null
}

# 3. 破解递归爆栈

递归方法,都有一个共同的问题, 那就是会爆栈,错误提示如下。

// RangeError: Maximum call stack size exceeded

那应该如何解决呢?其实我们使用循环就可以了,代码如下。

举个例子,假设有如下的数据结构

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

这不就是一个树吗,其实只要把数据横过来看就非常明显了

    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1       

用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点

首先我们往栈里放入种子数据,key用来存储放哪一个父元素的那一个子元素拷贝对象

然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝

function cloneLoop(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,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(s,
                        key: k,{
                        parent: re
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

# - JSON.stringify()

所有 安全的 JSON 值 (JSON-safe)都可以使用 JSON.stringify(..)字符串化。 安全的 JSON 值是指能够呈现为有效 JSON 格式的值。

下面敲黑板划重点:

为了简单起见, 我们来看看什么是 不安全的 JSON 值 。 undefinedfunctionsymbol (ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的 对象 都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。

JSON.stringify(..) 在对象中遇到 undefinedfunctionsymbol 时会自动将其忽略, 在 数组中则会返回 null (以保证单元位置不变)。

let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。

但是该方法有以下几个问题。

1、会忽略 undefined

2、会忽略 symbol

3、不能序列化函数

4、不能解决循环引用的对象

5、不能正确处理new Date()

6、不能处理正则

# 性能问题

尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。