# 对象的深浅拷贝

基本数据类型数据存储在栈中;引用数据类型存储的是该对象在栈中的引用,真实的数据存放在堆内存里(引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。)。

基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值(nullundefined,布尔值,数字和字符串)也是不会改变的。

基本类型比较的是值,引用类型比较的是栈内存的地址是否指向同一个堆内存对象。

var str = "abc";
console.log(str[1]="f");    // f
console.log(str);           // abc
1
2
3
  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的是基本类型的值,如果属性是引用类型则拷贝的是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象(逐个成员拷贝)。浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.

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

# 赋值和深浅拷贝的区别

  • 赋值:当新建一个变量然后赋值一个对象给它时,赋的其实是对象在栈中的地址,而不是堆中的数据,这样两个对象共用一份地址,所以两个对象就联动了。切忌误以为这是浅拷贝。啥数据类型的属性都受影响
var obj1 = {
    name: 'linjiaheng',
    language: ['vue', 'ts', 'js']
}
var obj2 = obj1
console.log(obj2 === obj1)
obj2.name = 'xielin';
obj2.language[2] = 'React';
console.log(obj2)
console.log(obj1)
1
2
3
4
5
6
7
8
9
10

img

  • 浅拷贝:拷贝后对象的基本数据类型不受影响,引用类型因为共用一块内存所以互相影响。基本数据类型不受影响(重新在堆中创建内存)

给最外层新增属性时,拷贝对象不会跟着新增属性,这点和赋值不同。

// 浅拷贝
var obj1 = {
    name: 'linjiaheng',
    language: ['vue', 'ts', 'js']
}
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[3] = 'nuxt';
function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
        return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

img

img

  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
function deepClone(obj) {
    var newObj = obj instanceof Array ? []:{}; // 考虑数组
    if(typeof obj !== 'object') {
        return obj;
    } else {
        for(var i in obj) {
            newObj[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
        }
    }
    return newObj;
}
1
2
3
4
5
6
7
8
9
10
11

# 浅拷贝实现方式

  1. Object.assign()

很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。针对深拷贝,需要使用其他方法,因为Object.assign()拷贝的是可枚举属性值。假如源值是一个对象的引用,它仅仅会复制其引用值。

例子:

const log = console.log;
function test() {
    let obj1 = { a: 0 , b: { c: 0}}; 
    let obj2 = Object.assign({}, obj1); 
    log(JSON.stringify(obj2));
    // { a: 0, b: { c: 0}} 

    obj1.a = 1; 
    log(JSON.stringify(obj1));
    // { a: 1, b: { c: 0}} 
    log(JSON.stringify(obj2));
    // { a: 0, b: { c: 0}} 

    obj2.a = 2; 
    log(JSON.stringify(obj1));
    // { a: 1, b: { c: 0}} 
    log(JSON.stringify(obj2));
    // { a: 2, b: { c: 0}}

    obj2.b.c = 3; 
    log(JSON.stringify(obj1));
    // { a: 1, b: { c: 3}} 
    log(JSON.stringify(obj2));
    // { a: 2, b: { c: 3}}

    // Deep Clone 
    obj1 = { a: 0 , b: { c: 0}}; 
    let obj3 = JSON.parse(JSON.stringify(obj1)); 
    obj1.a = 4; 
    obj1.b.c = 4; 
    log(JSON.stringify(obj3));
    // { a: 0, b: { c: 0}}
}
test()
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
var obj = {a: 1, b: 2}
var newObj = Object.assign({}, obj)
console.log(JSON.stringify(newObj))
obj.a = 3
console.log(JSON.stringify(newObj)) // 不变,对象属性只有一层为浅拷贝
console.log(obj === newObj) // false
1
2
3
4
5
6
  1. Array.prototype.concat()
let arr = [1, 3, {
    username: 'kobe'
}];
let arr2=arr.concat();    
arr2[2].username = 'wade';
console.log(arr);
1
2
3
4
5
6

img

  1. Array.prototype.slice()
  2. 展开运算符
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
1
2
3
4
5

# 深拷贝实现方式

  1. JSON.parse(JSON.stringify())正反序列化

这也是利用JSON.stringify将对象序列化成JSON字符串,再用JSON.parse反序列化把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringifyJSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(被忽略了)了。JSON.stringify()可以用来判断两个对象值是否相等。为什么会忽略呢,是因为JSON.stringfyundefinde,Symbol,函数会转成undefined,在转成对象时既然是undefined就被忽略了咯。

  • 这个方法缺点:

    • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
    • 会抛弃对象的constructor,所有的构造函数会指向Object
    • Date 日期调用了 toJSON() 将其转换为了 string 字符串(Date.toISOString()),因此会被当做字符串处理。
    • NaNInfinity 格式的数值及 null 都会被当做 null
    • 其他类型的对象,包括 RegExp/Map/Set/WeakMap/WeakSet等引用类型对象,会变成空对象
    // 构造函数
    function person(pname) {
        this.name = pname;
    }
    const Messi = new person('Messi');
    // 函数
    function say() {
        console.log('hi');
    };
    const oldObj = {
        a: say, // 被忽略了
        b: new Array(1), // [null]
        c: new RegExp('ab+c', 'i'), // 变为{}
        d: Messi,
        e: undefined, // 被忽略了
        f: null, // null
        g: Symbol(222), // 被忽略了
        h: new Date(), // 转成字符串
        p: NaN, // null
        q: [say,Symbol(111),undefined], // [null,null,null]
        z: new Set(1,2), // {}
        o: new Map() // {}
    };
    const newObj = JSON.parse(JSON.stringify(oldObj));
    // 无法复制函数
    console.log(newObj.a, oldObj.a); // undefined [Function: say]
    // 稀疏数组复制错误
    console.log(newObj.b[0], oldObj.b[0]); // null undefined
    // 无法复制正则对象
    console.log(newObj.c, oldObj.c); // {} /ab+c/i
    // 构造函数指向错误
    console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]
    
    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
    • 对象有循环引用会报错:
    const oldObj = {};
    oldObj.a = oldObj;
    const newObj = JSON.parse(JSON.stringify(oldObj));
    console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
    
    1
    2
    3
    4

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

    TIP

    这个存储空间需要可以存储key-value形式的数据,且key可以是一个引用类型,可以选择Map这种数据结构

    • 检查map中有无克隆过的对象
    • 有则直接返回
    • 没有将当前对象作为key,克隆对象作为value进行存储
    • 继续克隆
    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 (const key in target) {
                cloneTarget[key] = clone(target[key], map);
            }
            return cloneTarget;
        } else {
            return target;
        }
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  1. 手写递归

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

递归容易爆栈,会发生栈溢出问题

// 生成指定深度和每层广度的代码
function createData(deep, breadth) {
    var data = {};
    var temp = data;

    for (var i = 0; i < deep; i++) {
        temp = temp['data'] = {};
        for (var j = 0; j < breadth; j++) {
            temp[j] = j;
        }
    }

    return data;
}
createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

clone层很深的话栈就会溢出,但数据的广度不会造成溢出

clone(createData(1000)); // ok
clone(createData(10000)); // Maximum call stack size exceeded
clone(createData(10, 100000)); // ok 广度不会溢出
1
2
3

破解递归

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({
                        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
37
38
39
40
41
42
43
  1. MessageChannel用于深拷贝

所需拷贝的对象含有内置类型并且不包含函数,可以使用MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 赋值,深浅拷贝总结

  1. 赋值是完完全全把旧对象的地址赋值给了新对象,这个时候对旧对象进行增删改属性,都会对新对象造成联动影响。
  2. 浅拷贝只拷贝了最外一层,里面多层共用一份内存。所以最外一层如果删除或者增加属性对新对象不影响。
function shallowClone(o) {
    const obj = {};
    for ( let i in o) {
      obj[i] = o[i];
    }
    return obj;
}
// 被克隆对象
const oldObj = {
    a: 1,
    b: [ 'e', 'f', 'g' ],
    c: { h: { i: 2 } }
};
const newObj = shallowClone(oldObj);
oldObj.c = {}
console.log(newObj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

img

  1. 无论是哪种拷贝,拷贝后的对象和旧对象一定不相等。

# 工具库lodash的拷贝函数源码学习

深拷贝_.cloneDeep(obj)