JavaScript完美深拷贝

 
JavaScript数据类型的拷贝原理
先看看JS数据类型图(除了Object,其他都是基础类型):

 


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

 

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

    基本类型数据是否能拷贝?
    键和值都是基本类型的普通对象是否能拷贝?
    Symbol作为对象的key是否能拷贝?
    Date和RegExp对象类型是否能拷贝?
    Map和Set对象类型是否能拷贝?
    Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
    对象的原型是否能拷贝?
    不可枚举属性是否能拷贝?
    循环引用是否能拷贝?

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

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

存在的问题改进方案
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
        }

        //处理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)
}

注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。
深浅拷贝主要针对的是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



// 测试
console.log(obj);
const clonedObj = deepClone(obj)
console.log(clonedObj );
clonedObj === obj // false,返回的是一个新对象
clonedObj.arr === obj.arr // false,说明拷贝的不是引用
clonedObj.func === obj.func // false,说明function也复制了一份
clonedObj.proto // proto,可以取到原型的属性
复制代码

 

 可以发现我们的cloneObj对象和原来的obj对象一模一样,并且修改cloneObj对象的各个属性都不会对obj对象造成影响。

 参考:https://blog.csdn.net/cc18868876837/article/details/114918262

 

posted @   starcraft888  阅读(420)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示