← 返回首页
Javascript基础教程(四十六)
发表时间:2021-07-12 22:00:38
对象拷贝

在Javascript中当我们给一个集合中添加对象时,通常只是添加了对象的引用,简单来说就是一个对象的副本,它只分配一个引用。

例如:

    let stu = {
        name: 'zhangsan',
        age :18
    }

    let stuList = [];

    stuList.push(stu);
    stuList.push(stu);
    stuList.push(stu);

    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);

    })
    //更改对象属性
    stu.name = "李四";
    stu.age = 20;
    stuList.push(stu);
    console.log("-----更改学生姓名后------");
    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
    })

运行结果:
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
-----更改学生姓名后------
姓名:李四,年龄:20
姓名:李四,年龄:20
姓名:李四,年龄:20
姓名:李四,年龄:20

1.JSON.parse(JSON.stringify(object))实现拷贝

上例改写如下:

    let stu = {
        name: 'zhangsan',
        age :18
    }

    let stuList = [];

    stuList.push(JSON.parse(JSON.stringify(stu)));
    stuList.push(JSON.parse(JSON.stringify(stu)));
    stuList.push(JSON.parse(JSON.stringify(stu)));

    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);

    })

    stu.name = "李四";
    stu.age = 20;
    stuList.push(JSON.parse(JSON.stringify(stu)));
    console.log("-----更改学生姓名后------");
    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
    }) 

运行结果:
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
-----更改学生姓名后------
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
姓名:zhangsan,年龄:18
姓名:李四,年龄:20

但是,使用这种方法的弊端就是如果这个对象中不仅仅是简单的数据,而是定义了方法的话,那么这样的对象拷贝时定义的方法是无法拷贝的。

上例改写如下:

   let stu = {
        name: 'zhangsan',
        age :18,
        study: function(){
            console.log(this.name + " is good good study!");
        }
    }

    let stuList = [];
    stuList.push(JSON.parse(JSON.stringify(stu)));
    stuList.push(JSON.parse(JSON.stringify(stu)));
    stuList.push(JSON.parse(JSON.stringify(stu)));

    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
        s.study();
    })

    stu.name = "李四";
    stu.age = 20;
    stuList.push(JSON.parse(JSON.stringify(stu)));
    console.log("-----更改学生姓名后------");
    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
        s.study();
    })

运行结果:
姓名:zhangsan,年龄:18
Uncaught TypeError: s.study is not a function

2.Object.assign() 实现拷贝

在es6的语法中就有一个叫做object.assign()的方法,官方对于这个方法是这样解释的:Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源中的属性覆盖。后来的源的属性将类似地覆盖早先的属性。然后咱们就可以放心地复制了。

上例改写如下:

   let stu = {
        name: 'zhangsan',
        age :18,
        study: function(){
            console.log(this.name + " is good good study!");
        }
    }

    let stuList = [];
    stuList.push(Object.assign({},stu));
    stuList.push(Object.assign({},stu));
    stuList.push(Object.assign({},stu));

    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
        s.study();
    })

    stu.name = "李四";
    stu.age = 20;
    stuList.push(Object.assign({},stu));
    console.log("-----更改学生姓名后------");
    stuList.forEach(function(s,i){
        console.log("姓名:"+s.name+",年龄:"+s.age);
        s.study();
    })

运行结果:
姓名:zhangsan,年龄:18
zhangsan is good good study!
姓名:zhangsan,年龄:18
zhangsan is good good study!
姓名:zhangsan,年龄:18
zhangsan is good good study!
-----更改学生姓名后------
姓名:zhangsan,年龄:18
zhangsan is good good study!
姓名:zhangsan,年龄:18
zhangsan is good good study!
姓名:zhangsan,年龄:18
zhangsan is good good study!
姓名:李四,年龄:20
李四 is good good study!

3.通过递归实现自定义的深拷贝算法

在JavaScript中,基础类型值的复制是直接拷贝一份新的一模一样的数据,这两份数据相互独立,互不影响。而引用类型值(Object类型)的复制是传递对象的引用(也就是对象所在的内存地址,即指向对象的指针),相当于多个变量指向同一个对象,那么只要其中的一个变量对这个对象进行修改,其他的变量所指向的对象也会跟着修改(因为它们指向的是同一个对象)。

传统的深拷贝递归算法存在以下问题:

存在的问题 改进方案
1. 不能处理循环引用 使用 WeakMap 作为一个Hash表来进行查询
2. 只考虑了Object对象 当参数为 Date、RegExp 、Function、Map、Set,则直接生成一个新的实例返回
3. 属性名为Symbol的属性,丢失了不可枚举的属性 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys();注:Reflect.ownKeys(obj)相当于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
4. 原型上的属性 Object.getOwnPropertyDescriptors()设置属性描述对象,以及Object.create()方式继承原型链

我们可以通过递归算法设计一个完美的深拷贝算法。


   //测试一个完美版的,自定义的通过递归算法实现的深拷贝算法。
    function deepClone(target) {
        // WeakMap作为记录对象Hash表(用于防止循环引用)
        const map = new WeakMap()

        // 判断是否为object类型的辅助函数,减少重复代码
        function isObject(target) {
            return (typeof target === 'object' && target) || typeof target === 'function'
        }

        function clone(data) {

            // 基础类型直接返回值
            if (!isObject(data)) {
                return data
            }

            // 日期或者正则对象则直接构造一个新的对象返回
            if ([Date, RegExp].includes(data.constructor)) {
                return new data.constructor(data)
            }

            // 处理函数对象
            if (typeof data === 'function') {
                return new Function('return ' + data.toString())()
            }

            // 如果该对象已存在,则直接返回该对象
            const exist = map.get(data)
            if (exist) {
                return exist
            }

            //处理Array对象
            if (Array.isArray(data)) {
                let ary = [];
                for (let i = 0; i < data.length; i++) {
                    ary.push(clone(data[i]));
                }
                return ary;
            }

            // 处理Map对象
            if (data instanceof Map) {
                const result = new Map()
                map.set(data, result)
                data.forEach((val, key) => {
                    // 注意:map中的值为object的话也得深拷贝
                    if (isObject(val)) {
                        result.set(key, clone(val))
                    } else {
                        result.set(key, val)
                    }
                })
                return result
            }

            // 处理Set对象
            if (data instanceof Set) {
                const result = new Set()
                map.set(data, result)
                data.forEach(val => {
                    // 注意:set中的值为object的话也得深拷贝
                    if (isObject(val)) {
                        result.add(clone(val))
                    } else {
                        result.add(val)
                    }
                })
                return result
            }

            // 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
            const keys = Reflect.ownKeys(data)
            // 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
            const allDesc = Object.getOwnPropertyDescriptors(data)
            // 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
            const result = Object.create(Object.getPrototypeOf(data), allDesc)

            // 新对象加入到map中,进行记录
            map.set(data, result)

            // Object.create()是浅拷贝,所以要判断并递归执行深拷贝
            keys.forEach(key => {
                const val = data[key]
                if (isObject(val)) {
                    // 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
                    result[key] = clone(val)
                } else {
                    result[key] = val
                }
            })
            return result
        }

        return clone(target)  //返回了一个克隆对象。
    }


    let person = {
        name: 'zhangsan',
        age: 20,
        gender: '女',
        car: {
            brand: 'BMW',
            price: 200000,
            color: 'red'
        },
        eat: () => {
            console.log(this.name + "is eating now...");
        },
        assets: undefined
    }

    let p = {};

    p = deepClone(person);

    console.log(p);

    p.car.brand = "BENZ"

    console.log(p);
    console.log(person);

运行结果:

4.structuredClone实现深拷贝

JavaScript中添加了一个全局的方法,可以实现对象进行深拷贝了,没错那就是structuredClone()。

目前主流浏览器的最新版均已实现了此 API,Firefox 94,Chrome 98 已支持。此外,Node 17 和 Deno 1.14 也已实现了此 API。我们现在就可以放心使用这个函数了,不用担心太多。

structuredClone支持绝大多数JS对象,包括Array,Date,RegExp,Blob,FileList等等,但不支持Function,Error和DOM节点等引用类型。

实例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>

    let person = {
        name: 'zhangsan',
        age: 20,
        gender: '女',
        address: ['西安', '渭南', '汉中'],
        carList: [
            {
                brand: 'BMW',
                price: 200000,
                color: 'red'
            },
            {
                brand: 'BENZ',
                price: 300000,
                color: 'black'
            },
            {
                brand: 'TOYOTA',
                price: 10000,
                color: 'white'
            }
        ],
        assets: undefined
    }

    let p = structuredClone(person);

    console.log(p);
</script>
</body>
</html>

5.loadash.js实现深拷贝

Lodash是一个一致性、模块化、高性能的 JavaScript实用工具库。里面提供了深拷贝函数cloneDeep();

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--引入鲁大师库-->
    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>
<body>
<script>
    let person = {
        name: '张三丰',
        gender: '男',
        address: '湖北武当山',
        kill: function () {
            console.log('太极神功...');
        }
    }

    let obj = _.cloneDeep(person);
    console.log(obj);
    obj.kill();
</script>
</body>
</html>