说一说 深拷贝与浅拷贝

2,070 阅读6分钟

两者的区别直观图:

image.png

image.png

先来两段代码热热身:

  • 浅拷贝

        let Obj = {
            name: 'AAA',
            age: 18,
            haha:{
                a:'1254',
                b:()=>{
                    return 'b'
                },
                c:["sdad",'sad',1545]
            }
        }
        let obj5 = Object.assign({
            ...Obj
        }) 

        console.log(obj5);
    
        obj5.haha.c.push('6666')
        console.log(Obj)

image.png

  • 递归深拷贝:

let E = { id: 1, val: "111", arr: [1, 2, { name: '嘻嘻' }], info: { projectName: "项目1" } };
        let F = deepClone(E);
        console.log(E === F); // false
        console.log('E', E); // { id: 1, val: "111", arr:[1,2,{name:'嘻嘻'}], info: { projectName: "项目1" } }
        console.log('F', F); // { id: 1, val: "111", arr:[1,2,{name:'嘻嘻'}], info: { projectName: "项目1" } }

        let e = {
            name: '张三',
            sayGoodbye: function () {
                console.log('Goodbye!');
            }
        };
        let f = deepClone(e);
        e.sayGoodbye();//Goodbye!
        f.sayGoodbye();//Goodbye!
        
        
  
// 递归方法:
        function deepClone(source) {
            const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
            for (let keys in source) { // 遍历目标
                if (source.hasOwnProperty(keys)) {
                    if (source[keys] && typeof source[keys] === 'object') { // 如果值是对象,就递归一下
                        targetObj[keys] = source[keys].constructor === Array ? [] : {};
                        targetObj[keys] = deepClone(source[keys]);
                    } else { // 如果不是,就直接赋值
                        targetObj[keys] = source[keys];
                    }
                }
            }
            return targetObj;
        }

看起来都是赋值 那么有啥区别呢???

需求出发

比如在做一些表单提交的时候我们经常 需要再提交那个对象进行一些数据的更改 但又不想影响到页面双向数据绑定的那些属性 (因为如果提交失败 你修改的值会直接影响到页面的回显效果)那当你遇到需要提交一个很复杂的对象(各种类型的数据都有)那么是采用直接赋值还是 采用深拷贝的函数 ?

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

对象赋值

        var xiaoDeng  = {
            name:"lulu",
            age:18,
            flag:true,
            friends:['小李','小明','小红','小爱'],
            happy:{
                eat:'华莱士',
                study:['bilibili','baidu'],
                goTo:()=>{
                    return '去旅行'
                }
            },
            add:(x,y)=>{
                return x+y
            }
        }

        let newPeople = xiaoDeng

        console.log('newPeople:',newPeople);

image.png

修改 xiaoDeng.happy.study


        xiaoDeng.happy.study.splice(1,1)
        console.log('xiaoDeng:',xiaoDeng);
        console.log('newPeople:',newPeople);

image.png

两个都被修改了

浅拷贝

  let obj = {
            name: 'AAA',
            age: 18
        }

        // 1,新建新对象,复制原来对象的值    如果是引用类型 都会改变
        let obj1 = {
            name: obj.name,
            age: obj.age
        }
        obj1.name = 'BBB'
        // console.log(obj.name); // AAA
        // console.log(obj1.name); // BBB

        // 2,新建对象,循环添加
        // for (let key in obj) {
        //     obj2[key] = obj[key]
        // }

        function shallowClone(source) {
            var target = {};
            for (var i in source) {
                if (source.hasOwnProperty(i)) { //该方法会忽略掉那些从原型链上继承到的属性。
                    target[i] = source[i];
                }
            }
            return target;
        }
        let obj2 = shallowClone(obj1)
        console.log(obj.name); // AAA
        obj2.name = 'CCC'
        console.log(obj2.name); // CCC

        // 3, Object.assign()
        let obj3 = Object.assign({}, obj) // 第一个参数目标对象要加上
        obj3.name = 'DDD'
        console.log(obj.name); // AAA
        console.log(obj3.name); // DDD

        // 4,点语法展开

        // console.log({
        //     ...obj
        // })
        // {name: "AAA", age: 18}
        let obj4 = Object.assign({
            ...obj
        }) //只有目标对象。
        obj4.name = 'EEE'
        console.log(obj.name); // AAA
        console.log(obj4.name); // EEE

网上的这种说法其实不是很妥当的:

简单的理解就是拷贝了对象的第一层属性,如果对象的某个属性还有第二层,第三层的数据,浅拷贝是访问不到的。 比如说某个属性的值是对象,那浅拷贝无法复制该对象的数据

这能说 JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;

在 JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。同时,ES6 中 引入了 Object.assgn 方法和 … 展开运算符也能实现对对象的拷贝

concat
  let originArr = [1, [1, 2, 3], {
            a: 1,
            sayGoodbye: function () {
                console.log('Goodbye!');
            }
        }];
        let copyArr = originArr.concat();
        console.log(copyArr === originArr); // false
        console.log(copyArr)//concat不传参数 赋值一份相同的
        //修改复制出来的
        copyArr[2].sayGoodbye(); //Goodbye!
        copyArr[1].push(4);
        copyArr[2].a = 2;
        //原来的那一份也是被修改了
        console.log('originArr',originArr); // [1, [1, 2, 3, 4], {a: 2,sayGoodbye: function () {console.log('Goodbye!');}}]
        console.log('copyArr', copyArr); // [1, [1, 2, 3, 4], {a: 2,sayGoodbye: function () {console.log('Goodbye!');}}]

image.png

image.png originArr 中含有数组 [1,2,3] 和对象 {a:1},如果我们直接修改数组和对象,不会影响 originArr,但是我们修改数组 [1,2,3] 或对象 {a:1} 时,发现 originArr 也发生了变化。

结论: concat 只是对数组的第一层进行深拷贝。

slice(包含 begin,但不包含 end)

image.png

image.png

  • 局限性:
  1. 对象:
   var arr1 = [{            "name": "weifeng"        }, {            "name": "boy"        }]; //原数组
        var arr2 = [].concat(arr1); //拷贝数组
        arr1[1].name = "girl";
        console.log(arr1); // [{"name":"weifeng"},{"name":"girl"}]
        console.log(arr2); //[{"name":"weifeng"},{"name":"girl"}]

image.png 2. 数组: image.png

结论: slice 也是只对数组的第一层进行深拷贝。

结论:… 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。

结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

小结: 浅拷贝的实现方式

  1. Object.assign()
  2. 函数库lodash的_.clone方法
  3. 展开运算符…
  4. Array.prototype.concat()
  5. Array.prototype.slice() 等

深拷贝

那么又有几种呢?

1. JSON.parse(JSON.stringify())

image.png 利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

缺点:这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。 当深拷贝对象含有undefined、function、symbol 会在转换过程中会被忽略

  • 测试:
  // 深拷贝
        let obj1 = {
            name: '浪里行舟',
            arr: [1, [2, 3], 4],
            add:(x,y)=>{
                return x+y
            },
            str:/^[0-9]{1,20}$/

        }
        console.log(obj1)
        let newObj = JSON.stringify(obj1)
        console.log(JSON.parse(newObj))


image.png

2. 函数库lodash的_.cloneDeep方法 (开发必备)

遇到比较简单的对象我们们应该直接调用clone方法而不是cloneDeep(或者JSON.parse(JSON.stringify(obj))),这样既保险也可以减少性能损耗。

函数库:www.lodashjs.com/

浏览器使用:www.bootcdn.cn/lodash.js/

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
   
</head>

<body>



    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    <script>
        // var objects = [{
        //     'a': 1
        // }, {
        //     'b': 2
        // }];

        // var deep = _.cloneDeep(objects);
        // console.log(deep[0] === objects[0]);
        // => false
        var obj = {
            id: 1,
            name: {
                a: 'xx'
            },
            fn: function () {}
        };
        var obj2 = _.cloneDeep(obj);
        obj2.name.a = 'obj2';
        console.log(obj, obj2)


        const arr =  [5, 2, 3, 4, 1, "a", "c", "b", "d"]
        const solveArr = _.chunk(arr,2)
        console.log(solveArr)  
    </script>


</body>

</html>

image.png

3. jQuery.extend()方法

$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝

var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); 
// false

4. 手写递归方法

ps:对象存在循环引用的情况

方法一:相对全面:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
         let obj = {
            name: '1张三丰',
            age: 22,
            messige: {
                sex: '男',
                score: 16
            },
            color: ['red', 'purple', 'qing'],
            fig: true,
            fn: function (x) {
                console.log('我在打游戏' + x)
            },
            str: /^[0-9]{1,20}$/
        }
        let be = {};
        // Object.prototype.toString 会返回一个类型字符串 如未被重写则返回'[object String]'  被重写则返回类型
        function getType(obj) {
            //tostring会返回对应不同的标签的构造函数
            // var toString = Object.prototype.toString;
            var map = {
                '[object Boolean]': 'boolean',
                '[object Number]': 'number',
                '[object String]': 'string',
                '[object Function]': 'function',
                '[object Array]': 'array',
                '[object Date]': 'date',
                '[object RegExp]': 'regExp',
                '[object Undefined]': 'undefined',
                '[object Null]': 'null',
                '[object Object]': 'object'
            };
            return map[Object.prototype.toString.call(obj)];
        }
        function kao(be, obj) {
            for (var key in obj) {
                // 检测对象中是否有该key
                if (obj.hasOwnProperty(key)) {
                    let type = getType(obj[key])
                    if (type === 'object') {
                        be[key] = {}
                        kao(be[key], obj[key])
                    } else if (type === 'array') {
                        be[key] = []
                        kao(be[key], obj[key])
                    }else{
                        be[key]=obj[key]
                    }
                }
            }
        }
        kao(be, obj)
        console.log(be)
        console.log(obj)


        // let obj1 = {
        //     name: 1,
        //     address: {
        //         x: 100
        //     }
        // };

        // obj1.o = obj1; // 对象存在循环引用的情况
        // let d = kao({},obj1);
        // console.log(d);

    </script>
</body>
</html>
方法二:(个人推荐的)

        function deepClone(obj, hash = new WeakMap()) {
            if (obj === null) return obj;
            // 如果是null或者undefined我就不进行拷贝操作
            if (obj instanceof Date) return new Date(obj);
            if (obj instanceof RegExp) return new RegExp(obj);
            // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
            if (typeof obj !== "object") return obj;
            // 是对象的话就要进行深拷贝
            if (hash.get(obj)) return hash.get(obj);
            let cloneObj = new obj.constructor();
            // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
            hash.set(obj, cloneObj);
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    // 实现一个递归拷贝
                    cloneObj[key] = deepClone(obj[key], hash);
                }
            }
            return cloneObj;
        }
        let obj = {
            name: 1,
            address: {
                x: 100
            }
        };
        obj.o = obj; // 对象存在循环引用的情况
        let d = deepClone(obj);
        obj.address.x = 200;
        console.log(d);

image.png 可以复制正则 image.png

上面这方法是没有报错的!!!

方法三:(项目够用 )

不能复制正则 也是会无限循环!!!

 // 这种方式 适用于多层数据嵌套

        // export function deepClone(obj) { //可传入对象 或 数组
        function deepClone(obj) { //可传入对象 或 数组
            //  判断是否为 null 或 undefined 直接返回该值即可,
            if (obj === null || !obj) return obj;
            // 判断 是要深拷贝 对象 还是 数组
            if (Object.prototype.toString.call(obj) === "[object Object]") { //对象字符串化的值会为 "[object Object]"
                let target = {}; //生成新的一个对象
                const keys = Object.keys(obj); //取出对象所有的key属性 返回数组 keys = [ ]
                //遍历复制值, 可用 for 循环代替性能较好
                keys.forEach(key => {
                    if (obj[key] && typeof obj[key] === "object")
                        //如果遇到的值又是 引用类型的 [ ] {} ,得继续深拷贝
                        target[key] = deepClone(obj[key]); //递归
                    else
                        target[key] = obj[key];

                })
                return target //返回新的对象
            } else if (Array.isArray(obj)) {
                // 数组同理
                let arr = [];
                obj.forEach((item, index) => {
                    if (item && typeof item === "object")
                        arr[index] = deepClone(item);
                    else
                        arr[index] = item;
                })
                return arr
            }
        }

        // 用法
        let Obj = {
            name: 'AAA',
            age: 18,
            haha: {
                a: '1254',
                b: () => {
                    return 'b'
                },
                c: ["sdad", 'sad', 1545]
            },
            str: /^[0-9]{1,20}$/
        }
        let newObj = deepClone(Obj)
        console.log(newObj)//


        let obj = {
            name: 1,
            address: {
                x: 100
            }
        };
        // obj.o = obj; // 对象存在循环引用的情况
        // let d = deepClone(obj);
        // obj.address.x = 200;
        // console.log(d);

image.png image.png image.png

最后总结一下:

   1.赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
   2.JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
   3.JSON.stringify 实现的是深拷贝,但是对目标对象有要求(非 undefinedfunction);
   4.若想真正意义上的深拷贝,请递归。