小褚 发表于 2024-7-16 19:51:47

[JS] 深拷贝的实现

浅拷贝和深拷贝的区别


[*]浅拷贝:浅拷贝指的是复制一个对象的时候,对于对象的一个属性,

[*]如果是基本数据类型,则复制其值;
[*]如果是引用数据类型,则复制其引用。

[*]深拷贝:深拷贝指的是复制一个对象的时候,对于对象的一个属性,

[*]如果是基本数据类型,则复制其值;
[*]如果是引用数据类型,则递归地深拷贝该对象。

从内存的堆区和栈区上观察它们的区别:

[*]浅拷贝:引用数据类型的属性值指向同一个对象实例:

[*]深拷贝:引用数据类型的属性值在深拷贝的时候会在堆区申请新的空间,然后填入新内容(递归调用):

如何实现深拷贝

接口定义:deepClone(sourceObject): newObject
需要注意或者思考的问题:

[*]小心循环引用导致的无限递归导致栈溢出;
可以使用一个WeakMap记录已创建的对象,后续递归过程中如果引用了该对象,则直接填入其引用,而不是递归调用deepClone,从而避免了无限递归。
其中WeakMap用于记录源对象上的引用到目标对象上的引用的映射记录。因为在deepClone的过程中,我们是在使用DFS遍历source的过程中构建target。

[*]递归调用什么时候返回?

[*]基本数据类型直接返回;
[*]如果WeakMap上已经存在记录,说明存在循环引用,直接返回记录的引用,而不是递归调用。

[*]如何获取对象上的key,常规的key、Symbol类型的key、不可遍历的key如何获取?
直接使用Reflect.ownKeys可以获取对象上的常规key、Symbol-key和不可遍历的key。
[*]拷贝对象的时候,对象属性的描述符也要复制;
使用Reflect.getOwnPropertyDescriptor(target, key)方法可以获取target对象上key属性的描述符对象。
[*]对于特殊类型的引用数据类型,应考虑对应的复制方法,如Set和Map数据类型需要考虑添加记录的顺序
如何判断引用数据类型

可以使用typeof判断一个变量是否是object类型,使用typeof需要注意两个特例:

[*]typeof null === 'object',null是基本数据类型,但是会返回object;
[*]typeof function(){} === 'function',function是引用数据类型且是object的子类型,但是会返回function。
function isObject(target){
return (typeof target==='object' && target!==null) || typeof target==='function';
}代码

function deepClone(target){
// 提前记录clone的键值对,用于处理循环引用
const map = new WeakMap();

/**
   * 辅助函数:判断是否是对象类型
   * 需要注意`null` 和 `function`
   * @returns
   */
function isObject(target){
    return (typeof target === 'object' && target !== null)
    || typeof target === 'function'
}
function clone(target){
    /**
   * 基本数据类型
   * 操作:直接返回
   */
    if(!isObject(target))return target;

    /**
   * Date和RegExp对象类型
   * 操作:使用构造函数复制
   */
    if(.includes(target.constructor)){
      return new target.constructor(target);
    }

    /**
   * 函数类型
   */
    if(typeof target==='function'){
      return new Function('return ' + target.toString())();
    }

    /**
   * 数组类型
   */
    if(Array.isArray(target)){
      return target.map(el => clone(el));
    }

    /**
   * 检查是否存在循环引用
   */
    if(map.has(target))return map.get(target);

    /**
   * 处理Map对象类型
   */
    if(target instanceof Map){
      const res = new Map();
      map.set(target, res);
      target.forEach((val, key) => {
      // 如果Map中的val是对象,也得深拷贝
      res.set(key, isObject(val) ? clone(val) : val);
      })
      return res;
    }

    /**
   * 处理Set对象类型
   */
    if(target instanceof Set){
      const res = new Set();
      map.set(target, res);
      target.forEach(val => {
      // 如果val是对象类型,则递归深拷贝
      res.add(isObject(val) ? clone(val) : val);
      })
      return res;
    }

    //==========================================
    // 接下来是常规对象类型
    //==========================================
   
    // 收集key(包括Symbol和不可枚举的属性)
    const keys = Reflect.ownKeys(target);
    // 收集各个key的描述符
    const allDesc = {};
    keys.forEach(key => {
      allDesc = Reflect.getOwnPropertyDescriptor(target, key);
    })
    // 创建新对象(浅拷贝)
    const res = Reflect.construct(Reflect.getPrototypeOf(target).constructor, []);
    // 在递归调用clone之前记录新对象,避免循环
    map.set(target, res);
    // 赋值并检查是否val是否为对象类型
    keys.forEach(key => {
      // 添加对象描述符
      Reflect.defineProperty(res, key, allDesc);
      // 赋值
      const val = target;
      res = isObject(val) ? clone(val) : val;
    });
    return res;
}

return clone(target);
}使用jest测试

安装jest

pnpm install jest --save-dev这里我使用的版本是:
{
    ...
    "devDependencies": {
            "jest": "^29.7.0"
    },
    ...
}指令

package.json
{
    ...
    "scripts": {
      "test": "jest"
    },
    ...
}编写测试用例

deepClone.test.js
const deepClone = require('./deepClone');

test('deep clone primitive types', () => {
expect(deepClone(42)).toBe(42);
expect(deepClone('hello')).toBe('hello');
expect(deepClone(null)).toBeNull();
expect(deepClone(undefined)).toBeUndefined();
expect(deepClone(true)).toBe(true);
});

test('deep clone array', () => {
const arr = ];
const clonedArr = deepClone(arr);
expect(clonedArr).toEqual(arr);
expect(clonedArr).not.toBe(arr);
expect(clonedArr).not.toBe(arr);
expect(clonedArr).not.toBe(arr);
});

test('deep clone object', () => {
const obj = { a: 1, b: { c: 2 } };
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj).not.toBe(obj);
expect(clonedObj.b).not.toBe(obj.b);
});

test('deep clone Map', () => {
const map = new Map();
map.set('a', 1);
map.set('b', { c: 2 });
const clonedMap = deepClone(map);
expect(clonedMap).toEqual(map);
expect(clonedMap).not.toBe(map);
expect(clonedMap.get('b')).not.toBe(map.get('b'));
});

test('deep clone Set', () => {
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const set = new Set();
const clonedSet = deepClone(set);
expect(clonedSet).toEqual(set);
expect(clonedSet).not.toBe(set);
expect(clonedSet.has(1)).toBe(true);
expect(clonedSet.has('string')).toBe(true);
const clonedObj1 = Array.from(clonedSet).find(item => typeof item === 'object' && item.a === 1);
const clonedObj2 = Array.from(clonedSet).find(item => typeof item === 'object' && item.b === 2);

expect(clonedObj1).toEqual(obj1);
expect(clonedObj1).not.toBe(obj1);

expect(clonedObj2).toEqual(obj2);
expect(clonedObj2).not.toBe(obj2);
});

test('deep clone with Symbol keys', () => {
const sym = Symbol('key');
const obj = { : 1, a: 2 };
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj).toBe(1);
expect(clonedObj.a).toBe(2);
});

test('deep clone with non-enumerable properties', () => {
const obj = {};
Object.defineProperty(obj, 'a', { value: 1, enumerable: false });
const clonedObj = deepClone(obj);
expect(clonedObj).toHaveProperty('a', 1);
expect(Object.keys(clonedObj)).not.toContain('a');
});

test('deep clone with property descriptors', () => {
const obj = {};
Object.defineProperty(obj, 'a', {
    value: 1,
    writable: false,
    configurable: false,
    enumerable: true
});
const clonedObj = deepClone(obj);
const desc = Object.getOwnPropertyDescriptor(clonedObj, 'a');
expect(desc.value).toBe(1);
expect(desc.writable).toBe(false);
expect(desc.configurable).toBe(false);
expect(desc.enumerable).toBe(true);
});

test('deep clone circular references', () => {
const obj = { a: 1 };
obj.self = obj;
const clonedObj = deepClone(obj);
expect(clonedObj).toEqual(obj);
expect(clonedObj.self).toBe(clonedObj);
expect(clonedObj.self).not.toBe(obj);
});测试结果

npm run test

来源:https://www.cnblogs.com/feixianxing/p/18305861/javascript-deep-clone-and-shallow-clone
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: [JS] 深拷贝的实现