谈谈js-深浅拷贝【含手撕深拷贝】

50 阅读6分钟

一、数据类型

  • 基本数据类型

    Number、String、Boolean、Null、undefined、Symbol、Bigint

  • 引用数据类型

    Object、Array、Function

存储方式

x1.png

二、赋值(Copy)

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

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

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

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

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

        let a = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            }
        }
        let b = a;
        console.log(b);
        // {
        // 	name: "fzw",
        // 	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: "change",
        // 	book: {title: "You Don't Know JS", price: "55"}
        // } 

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

三、浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。

可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址

3.1 Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

有些文章说Object.assign() 是深拷贝,其实这是不正确的。

    let a = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            }
        }
        let b = Object.assign({}, a);
        console.log(b);
        // {
        // 	name: "fzw",
        // 	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: "fzw",
        // 	book: {title: "You Don't Know JS", price: "55"}
        // } 

上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

3.2 扩展运算符

let b = Object.assign({}, a); 换成 let b = {...a};

通过代码可以看出实际效果和 Object.assign() 是一样的。

3.3 Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

        let a = [0, "1", [2, 3]];
        let b = a.slice(1);
        console.log(b);
        // ["1", [2, 3]]

        a[1] = "99";
        a[2][0] = 4;
        console.log(a);
        // [0, "99", [4, 3]]

        console.log(b);
        //  ["1", [4, 3]]

可以看出,改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

实际上对于数组来说, 只要不修改原数组, 重新返回一个新数组就可以实现浅拷贝,比如说concat、map、filter、reduce等方法

四、深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

4.1 JSON.parse(JSON.stringify(object))

         let a = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            }
        }
        let b = JSON.parse(JSON.stringify(a));
        console.log(b);
        // {
        // 	name: "fzw",
        // 	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: "fzw",
        // 	book: {title: "You Don't Know JS", price: "45"}
        // } 

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

我们看下对数组深拷贝效果如何。

        let a = [0, "1", [2, 3]];
        let b = JSON.parse(JSON.stringify(a.slice(1)));
        console.log(b);
        // ["1", [2, 3]]

        a[1] = "99";
        a[2][0] = 4;
        console.log(a);
        // [0, "99", [4, 3]]

        console.log(b);
        //  ["1", [2, 3]]

对数组深拷贝之后,改变原数组不会影响到拷贝之后的数组。

注意:使用该方法有以下几个问题

  1. 会忽略 undefined
  2. 会忽略 symbol
  3. 不能序列化函数
  4. 不能解决循环引用的对象
  5. 不能正确处理new Date()
  6. 不能处理正则
  • undefinedsymbol 和函数这三种情况,会直接忽略。
        let obj = {
            name: 'fzw',
            a: undefined,
            b: Symbol('fzw'),
            c: function () { }
        }
        console.log(obj);
        // {
        // 	name: "fzw", 
        // 	a: undefined, 
        //  b: Symbol(fzw), 
        //  c: ƒ ()
        // }

        let b = JSON.parse(JSON.stringify(obj));
        console.log(b);
        // {name: "fzw"}
  • 循环引用情况下,会报错。
        let obj = {
            a: 1,
            b: {
                c: 2,
                d: 3
            }
        }
        obj.a = obj.b;
        obj.b.c = obj.a;

        let b = JSON.parse(JSON.stringify(obj));
        // Uncaught TypeError: Converting circular structure to JSON
  • new Date 情况下,转换结果不正确。
        new Date();
        // Thu May 11 2023 15:16:42 GMT+0800 (中国标准时间)
        JSON.stringify(new Date());
        // "2023-05-11T07:16:42.388Z"
        JSON.parse(JSON.stringify(new Date()));
        // 2023-05-11T07:16:42.388Z

解决方法转成字符串或者时间戳就好了。

        let date = (new Date()).valueOf();
        // 1683789622169
        JSON.stringify(date);
        // "1683789622169"
        JSON.parse(JSON.stringify(date));
        // 1683789622169
  • 正则情况下
        let obj = {
            name: "fzw",
            a: /'123'/
        }
        console.log(obj);
        // {name: "fzw", a: /'123'/}

        let b = JSON.parse(JSON.stringify(obj));
        console.log(b);
        // {name: "fzw", a: {}}

4.2 深拷贝(手写)

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

基础版

  • 判断传入对象是否为 object 且不为 null
        function isObject(target) {
            return typeof target === 'object' && target !== null
        }
        function cloneDeep(source) {
            if (!isObject(source)) return source;
            // 考虑数组的兼容
            var target = Array.isArray(source) ? [] : {}
            for (let key in source) {
                if (source.hasOwnProperty(key)) {
                    if (isObject(source[key])) {
                        target[key] = cloneDeep(source[key])
                    } else {
                        target[key] = source[key]
                    }
                }
            }
            return target;
        }

        function isObject(target) {
            return typeof target === 'object' && target !== null
        }

        var test1 = 'fzw'
        var test2 = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            },
            a1: undefined,
            a2: null,
            a3: 123
        }

        var copyResult = cloneDeep(test2)
        console.log(copyResult);

解决循环引用

        var test = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            },
            a1: undefined,
            a2: null,
            a3: 123
        }
        test.circleRef = test;

        var copyResult = JSON.parse(JSON.stringify(test));
        console.log(copyResult);
        // Uncaught TypeError: Converting circular structure to JSON

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

    function cloneDeep(source, hash = new WeakMap()) {
            if (!isObject(source)) return source;
            if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
            var target = Array.isArray(source) ? [] : {}
            hash.set(source, target); // 新增代码,哈希表设值
            for (let key in source) {
                if (source.hasOwnProperty(key)) {
                    if (isObject(source[key])) {
                        target[key] = cloneDeep(source[key], hash)
                    } else {
                        target[key] = source[key]
                    }
                }
            }
            return target;
        }

        function isObject(target) {
            return typeof target === 'object' && target !== null
        }

        var test1 = 'fzw'
        var test2 = {
            name: "fzw",
            book: {
                title: "You Don't Know JS",
                price: "45"
            },
            a1: undefined,
            a2: null,
            a3: 123
        }
        test2.circleRef = test2;
        var copyResult = cloneDeep(test2)

        console.log(copyResult);

五、总结

--和原数据是否指向同一对象第一层数据为基本数据类型原数据中包含子对象
赋值改变会使原数据一同改变改变会使原数据一同改变
浅拷贝改变会使原数据一同改变改变会使原数据一同改变
深拷贝改变会使原数据一同改变改变会使原数据一同改变
x2.jpg