翼度科技»论坛 编程开发 JavaScript 查看内容

[JS] 深拷贝的实现

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
浅拷贝和深拷贝的区别


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

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

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

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

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

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

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

如何实现深拷贝

接口定义: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。
  1. function isObject(target){
  2.   return (typeof target==='object' && target!==null) || typeof target==='function';
  3. }
复制代码
代码
  1. function deepClone(target){
  2.   // 提前记录clone的键值对,用于处理循环引用
  3.   const map = new WeakMap();
  4.   /**
  5.    * 辅助函数:判断是否是对象类型  
  6.    * 需要注意`null` 和 `function`
  7.    * @returns
  8.    */
  9.   function isObject(target){
  10.     return (typeof target === 'object' && target !== null)
  11.     || typeof target === 'function'
  12.   }
  13.   function clone(target){
  14.     /**
  15.      * 基本数据类型
  16.      * 操作:直接返回
  17.      */
  18.     if(!isObject(target))return target;
  19.     /**
  20.      * Date和RegExp对象类型
  21.      * 操作:使用构造函数复制
  22.      */
  23.     if([Date, RegExp].includes(target.constructor)){
  24.       return new target.constructor(target);
  25.     }
  26.     /**
  27.      * 函数类型
  28.      */
  29.     if(typeof target==='function'){
  30.       return new Function('return ' + target.toString())();
  31.     }
  32.     /**
  33.      * 数组类型
  34.      */
  35.     if(Array.isArray(target)){
  36.       return target.map(el => clone(el));
  37.     }
  38.     /**
  39.      * 检查是否存在循环引用
  40.      */
  41.     if(map.has(target))return map.get(target);
  42.     /**
  43.      * 处理Map对象类型
  44.      */
  45.     if(target instanceof Map){
  46.       const res = new Map();
  47.       map.set(target, res);
  48.       target.forEach((val, key) => {
  49.         // 如果Map中的val是对象,也得深拷贝
  50.         res.set(key, isObject(val) ? clone(val) : val);
  51.       })
  52.       return res;
  53.     }
  54.     /**
  55.      * 处理Set对象类型
  56.      */
  57.     if(target instanceof Set){
  58.       const res = new Set();
  59.       map.set(target, res);
  60.       target.forEach(val => {
  61.         // 如果val是对象类型,则递归深拷贝
  62.         res.add(isObject(val) ? clone(val) : val);
  63.       })
  64.       return res;
  65.     }
  66.     //==========================================
  67.     // 接下来是常规对象类型
  68.     //==========================================
  69.    
  70.     // 收集key(包括Symbol和不可枚举的属性)
  71.     const keys = Reflect.ownKeys(target);
  72.     // 收集各个key的描述符
  73.     const allDesc = {};
  74.     keys.forEach(key => {
  75.       allDesc[key] = Reflect.getOwnPropertyDescriptor(target, key);
  76.     })
  77.     // 创建新对象(浅拷贝)
  78.     const res = Reflect.construct(Reflect.getPrototypeOf(target).constructor, []);
  79.     // 在递归调用clone之前记录新对象,避免循环
  80.     map.set(target, res);
  81.     // 赋值并检查是否val是否为对象类型
  82.     keys.forEach(key => {
  83.       // 添加对象描述符
  84.       Reflect.defineProperty(res, key, allDesc[key]);
  85.       // 赋值
  86.       const val = target[key];
  87.       res[key] = isObject(val) ? clone(val) : val;
  88.     });
  89.     return res;
  90.   }
  91.   return clone(target);
  92. }
复制代码
使用jest测试

安装jest
  1. pnpm install jest --save-dev
复制代码
这里我使用的版本是:
  1. {
  2.     ...
  3.     "devDependencies": {
  4.             "jest": "^29.7.0"  
  5.     },
  6.     ...
  7. }
复制代码
指令

package.json
  1. {
  2.     ...
  3.     "scripts": {
  4.         "test": "jest"
  5.     },
  6.     ...
  7. }
复制代码
编写测试用例

deepClone.test.js
  1. const deepClone = require('./deepClone');
  2. test('deep clone primitive types', () => {
  3.   expect(deepClone(42)).toBe(42);
  4.   expect(deepClone('hello')).toBe('hello');
  5.   expect(deepClone(null)).toBeNull();
  6.   expect(deepClone(undefined)).toBeUndefined();
  7.   expect(deepClone(true)).toBe(true);
  8. });
  9. test('deep clone array', () => {
  10.   const arr = [1, { a: 2 }, [3, 4]];
  11.   const clonedArr = deepClone(arr);
  12.   expect(clonedArr).toEqual(arr);
  13.   expect(clonedArr).not.toBe(arr);
  14.   expect(clonedArr[1]).not.toBe(arr[1]);
  15.   expect(clonedArr[2]).not.toBe(arr[2]);
  16. });
  17. test('deep clone object', () => {
  18.   const obj = { a: 1, b: { c: 2 } };
  19.   const clonedObj = deepClone(obj);
  20.   expect(clonedObj).toEqual(obj);
  21.   expect(clonedObj).not.toBe(obj);
  22.   expect(clonedObj.b).not.toBe(obj.b);
  23. });
  24. test('deep clone Map', () => {
  25.   const map = new Map();
  26.   map.set('a', 1);
  27.   map.set('b', { c: 2 });
  28.   const clonedMap = deepClone(map);
  29.   expect(clonedMap).toEqual(map);
  30.   expect(clonedMap).not.toBe(map);
  31.   expect(clonedMap.get('b')).not.toBe(map.get('b'));
  32. });
  33. test('deep clone Set', () => {
  34.   const obj1 = { a: 1 };
  35.   const obj2 = { b: 2 };
  36.   const set = new Set([1, 'string', obj1, obj2]);
  37.   const clonedSet = deepClone(set);
  38.   expect(clonedSet).toEqual(set);
  39.   expect(clonedSet).not.toBe(set);
  40.   expect(clonedSet.has(1)).toBe(true);
  41.   expect(clonedSet.has('string')).toBe(true);
  42.   const clonedObj1 = Array.from(clonedSet).find(item => typeof item === 'object' && item.a === 1);
  43.   const clonedObj2 = Array.from(clonedSet).find(item => typeof item === 'object' && item.b === 2);
  44.   expect(clonedObj1).toEqual(obj1);
  45.   expect(clonedObj1).not.toBe(obj1);
  46.   expect(clonedObj2).toEqual(obj2);
  47.   expect(clonedObj2).not.toBe(obj2);
  48. });
  49. test('deep clone with Symbol keys', () => {
  50.   const sym = Symbol('key');
  51.   const obj = { [sym]: 1, a: 2 };
  52.   const clonedObj = deepClone(obj);
  53.   expect(clonedObj).toEqual(obj);
  54.   expect(clonedObj[sym]).toBe(1);
  55.   expect(clonedObj.a).toBe(2);
  56. });
  57. test('deep clone with non-enumerable properties', () => {
  58.   const obj = {};
  59.   Object.defineProperty(obj, 'a', { value: 1, enumerable: false });
  60.   const clonedObj = deepClone(obj);
  61.   expect(clonedObj).toHaveProperty('a', 1);
  62.   expect(Object.keys(clonedObj)).not.toContain('a');
  63. });
  64. test('deep clone with property descriptors', () => {
  65.   const obj = {};
  66.   Object.defineProperty(obj, 'a', {
  67.     value: 1,
  68.     writable: false,
  69.     configurable: false,
  70.     enumerable: true
  71.   });
  72.   const clonedObj = deepClone(obj);
  73.   const desc = Object.getOwnPropertyDescriptor(clonedObj, 'a');
  74.   expect(desc.value).toBe(1);
  75.   expect(desc.writable).toBe(false);
  76.   expect(desc.configurable).toBe(false);
  77.   expect(desc.enumerable).toBe(true);
  78. });
  79. test('deep clone circular references', () => {
  80.   const obj = { a: 1 };
  81.   obj.self = obj;
  82.   const clonedObj = deepClone(obj);
  83.   expect(clonedObj).toEqual(obj);
  84.   expect(clonedObj.self).toBe(clonedObj);
  85.   expect(clonedObj.self).not.toBe(obj);
  86. });
复制代码
测试结果
  1. npm run test
复制代码


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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具