• 技术文章 >web前端 >js教程

    一文带你详细了解JavaScript中的深拷贝

    青灯夜游青灯夜游2022-10-21 19:37:55转载281

    大前端零基础入门到就业:进入学习

    网上有很多关于深拷贝的文章,但是质量良莠不齐,有很多都考虑得不周到,写的方法比较简陋,难以令人满意。本文旨在完成一个完美的深拷贝,大家看了如果有问题,欢迎一起补充完善。

    评价一个深拷贝是否完善,请检查以下问题是否都实现了:

    怎样?你写的深拷贝够完善吗?

    深拷贝的最终实现

    这里先直接给出最终的代码版本,方便想快速了解的人查看,当然,你想一步步了解可以继续查看文章余下的内容:

    function deepClone(target) {
        const map = new WeakMap()
        
        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
            }
            if (data instanceof Map) {
                const result = new Map()
                map.set(data, result)
                data.forEach((val, key) => {
                    if (isObject(val)) {
                        result.set(key, clone(val))
                    } else {
                        result.set(key, val)
                    }
                })
                return result
            }
            if (data instanceof Set) {
                const result = new Set()
                map.set(data, result)
                data.forEach(val => {
                    if (isObject(val)) {
                        result.add(clone(val))
                    } else {
                        result.add(val)
                    }
                })
                return result
            }
            const keys = Reflect.ownKeys(data)
            const allDesc = Object.getOwnPropertyDescriptors(data)
            const result = Object.create(Object.getPrototypeOf(data), allDesc)
            map.set(data, result)
            keys.forEach(key => {
                const val = data[key]
                if (isObject(val)) {
                    result[key] = clone(val)
                } else {
                    result[key] = val
                }
            })
            return result
        }
    
        return clone(target)
    }

    1. JavaScript数据类型的拷贝原理

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

    2. 深浅拷贝

    深浅拷贝主要针对的是Object类型,基础类型的值本身即是复制一模一样的一份,不区分深浅拷贝。这里我们先给出测试的拷贝对象,大家可以拿这个obj对象来测试一下自己写的深拷贝函数是否完善:

    // 测试的obj对象
    const obj = {
        // =========== 1.基础数据类型 ===========
        num: 0, // number
        str: '', // string
        bool: true, // boolean
        unf: undefined, // undefined
        nul: null, // null
        sym: Symbol('sym'), // symbol
        bign: BigInt(1n), // bigint
    
        // =========== 2.Object类型 ===========
        // 普通对象
        obj: {
            name: '我是一个对象',
            id: 1
        },
        // 数组
        arr: [0, 1, 2],
        // 函数
        func: function () {
            console.log('我是一个函数')
        },
        // 日期
        date: new Date(0),
        // 正则
        reg: new RegExp('/我是一个正则/ig'),
        // Map
        map: new Map().set('mapKey', 1),
        // Set
        set: new Set().add('set'),
        // =========== 3.其他 ===========
        [Symbol('1')]: 1  // Symbol作为key
    };
    
    // 4.添加不可枚举属性
    Object.defineProperty(obj, 'innumerable', {
        enumerable: false,
        value: '不可枚举属性'
    });
    
    // 5.设置原型对象
    Object.setPrototypeOf(obj, {
        proto: 'proto'
    })
    
    // 6.设置loop成循环引用的属性
    obj.loop = obj

    obj对象在Chrome浏览器中的结果:

    obj

    2.1 浅拷贝

    浅拷贝: 创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址所指向的对象,肯定会影响到另一个对象。

    首先我们看看一些浅拷贝的方法(详细了解可点击对应方法的超链接):

    方法使用方式注意事项
    Object.assign()Object.assign(target, ...sources)
    说明:用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
    1.不会拷贝对象的继承属性;
    2.不会拷贝对象的不可枚举的属性;
    3.可以拷贝 Symbol 类型的属性。
    展开语法let objClone = { ...obj };缺陷和Object.assign()差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
    Array.prototype.concat()拷贝数组const new_array = old_array.concat(value1[, value2[, ...[, valueN]]])浅拷贝,适用于基本类型值的数组
    Array.prototype.slice()拷贝数组arr.slice([begin[, end]])浅拷贝,适用于基本类型值的数组

    这里只列举了常用的几种方式,除此之外当然还有其他更多的方式。注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。

    手动实现一个浅拷贝:

    function 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;
        }
    }
    
    
    // 测试
    const shallowCloneObj = shallowClone(obj)
    
    shallowCloneObj === obj  // false,返回的是一个新对象
    shallowCloneObj.arr === obj.arr  // true,对于对象类型只拷贝了引用

    从上面这段代码可以看出,利用类型判断(查看typeof),针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性(for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包含原型上的属性。查看for…in),基本就可以手工实现一个浅拷贝的代码了。

    2.2 深拷贝

    深拷贝:创建一个新的对象,将一个对象从内存中完整地拷贝出来一份给该新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

    看看现存的一些深拷贝的方法:

    方法1:JSON.stringify()

    JSON.stringfy() 其实就是将一个 JavaScript 对象或值转换为 JSON 字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。(点这了解:JSON.stringfy()、JSON.parse())

    使用如下:

    function deepClone(target) {
        if (typeof target === 'object' && target !== null) {
            return JSON.parse(JSON.stringify(target));
        } else {
            return target;
        }
    }
    
    // 开头的测试obj存在BigInt类型、循环引用,JSON.stringfy()执行会报错,所以除去这两个条件进行测试
    const clonedObj = deepClone(obj)
    
    // 测试
    clonedObj === obj  // false,返回的是一个新对象
    clonedObj.arr === obj.arr  // false,说明拷贝的不是引用

    浏览器执行结果:

    浏览器执行JSON.stringify()结果
    从以上结果我们可知JSON.stringfy() 存在以下一些问题:

    由于以上种种限制条件,JSON.stringfy() 方式仅限于深拷贝一些普通的对象,对于更复杂的数据类型,我们需要另寻他路。

    方法2:递归基础版深拷贝

    手动递归实现深拷贝,我们只需要完成以下2点即可:

    function deepClone(target) {
        if (typeof target === 'object' && target) {
            let cloneObj = {}
            for (const key in target) { // 遍历
                const val = target[key]
                if (typeof val === 'object' && val) {
                    cloneObj[key] = deepClone(val) // 是对象就再次调用该函数递归
                } else {
                    cloneObj[key] = val // 基本类型的话直接复制值
                }
            }
            return cloneObj
        } else {
            return target;
        }
    }
    
    // 开头的测试obj存在循环引用,除去这个条件进行测试
    const clonedObj = deepClone(obj)
    
    // 测试
    clonedObj === obj  // false,返回的是一个新对象
    clonedObj.arr === obj.arr  // false,说明拷贝的不是引用

    浏览器执行结果:

    简版深拷贝
    该基础版本存在许多问题:

    如果存在循环引用的话,以上代码会导致无限递归,从而使得堆栈溢出。如下例子:

    const a = {}
    const b = {}
    a.b = b
    b.a = a
    deepClone(a)

    对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,查看a对象,可以看到是无限循环的:
    在这里插入图片描述
    对对象a执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出
    在这里插入图片描述
    如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(WeakMap相关见这:WeakMap)

    针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝

    方法3:递归完美版深拷贝

    对于基础版深拷贝存在的问题,我们一一改进:

    存在的问题改进方案
    1. 不能处理循环引用使用 WeakMap 作为一个Hash表来进行查询
    2. 只考虑了Object对象当参数为 DateRegExpFunctionMapSet,则直接生成一个新的实例返回
    3. 属性名为Symbol的属性
    4. 丢失了不可枚举的属性
    针对能够遍历对象的不可枚举属性以及 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
            }
    
            // 处理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)
    }
    
    
    
    // 测试
    const clonedObj = deepClone(obj)
    clonedObj === obj  // false,返回的是一个新对象
    clonedObj.arr === obj.arr  // false,说明拷贝的不是引用
    clonedObj.func === obj.func  // false,说明function也复制了一份
    clonedObj.proto  // proto,可以取到原型的属性

    详细的说明见代码中的注释,更多测试希望大家自己动手尝试验证一下以加深印象。

    在遍历 Object 类型数据时,我们需要把 Symbol 类型的键名也考虑进来,所以不能通过 Object.keys 获取键名或 for...in 方式遍历,而是通过Reflect.ownKeys()获取所有自身的键名(getOwnPropertyNamesgetOwnPropertySymbols 函数将键名组合成数组也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]),然后再遍历递归,最终实现拷贝。

    浏览器执行结果:
    完美版深拷贝
    可以发现我们的cloneObj对象和原来的obj对象一模一样,并且修改cloneObj对象的各个属性都不会对obj对象造成影响。其他的大家再多尝试体会哦!

    【相关推荐:javascript视频教程编程视频

    以上就是一文带你详细了解JavaScript中的深拷贝的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:csdn,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    快捷开发Web应用及小程序:点击使用

    支持亿级表,高并发,自动生成可视化后台。

    专题推荐:javascript
    上一篇:这款JS轻量编辑器能助你快速处理图片! 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• JavaScript怎么输入N个数据求平均数• 一文详解JavaScript中执行上下文与执行栈(图文结合)• javascript怎么求总分和平均值• javascript有没有返回值• javascript的COM对象是什么
    1/1

    PHP中文网