首页 > 其他分享 >[JS] 深拷贝的实现

[JS] 深拷贝的实现

时间:2024-07-16 18:31:49浏览次数:10  
标签:deepClone const target 实现 JS toBe expect key 拷贝

浅拷贝和深拷贝的区别

  • 浅拷贝:浅拷贝指的是复制一个对象的时候,对于对象的一个属性,
    • 如果是基本数据类型,则复制其值;
    • 如果是引用数据类型,则复制其引用。
  • 深拷贝:深拷贝指的是复制一个对象的时候,对于对象的一个属性,
    • 如果是基本数据类型,则复制其值;
    • 如果是引用数据类型,则递归地深拷贝该对象。

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

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

    image-20240716165302305

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

    image-20240716165429589

如何实现深拷贝

接口定义:deepClone(sourceObject): newObject

需要注意或者思考的问题:

  1. 小心循环引用导致的无限递归导致栈溢出;

    可以使用一个WeakMap记录已创建的对象,后续递归过程中如果引用了该对象,则直接填入其引用,而不是递归调用deepClone,从而避免了无限递归。

    其中WeakMap<source, target>用于记录源对象上的引用到目标对象上的引用的映射记录。因为在deepClone的过程中,我们是在使用DFS遍历source的过程中构建target

  2. 递归调用什么时候返回?

    • 基本数据类型直接返回;
    • 如果WeakMap上已经存在记录,说明存在循环引用,直接返回记录的引用,而不是递归调用。
  3. 如何获取对象上的key,常规的key、Symbol类型的key、不可遍历的key如何获取?

    直接使用Reflect.ownKeys可以获取对象上的常规key、Symbol-key和不可遍历的key。

  4. 拷贝对象的时候,对象属性的描述符也要复制;

    使用Reflect.getOwnPropertyDescriptor(target, key)方法可以获取target对象上key属性的描述符对象。

  5. 对于特殊类型的引用数据类型,应考虑对应的复制方法,如SetMap数据类型需要考虑添加记录的顺序

如何判断引用数据类型

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

  1. typeof null === 'object'null是基本数据类型,但是会返回object
  2. 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([Date, RegExp].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[key] = 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[key]);
      // 赋值
      const val = target[key];
      res[key] = 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 = [1, { a: 2 }, [3, 4]];
  const clonedArr = deepClone(arr);
  expect(clonedArr).toEqual(arr);
  expect(clonedArr).not.toBe(arr);
  expect(clonedArr[1]).not.toBe(arr[1]);
  expect(clonedArr[2]).not.toBe(arr[2]);
});

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([1, 'string', obj1, obj2]);
  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 = { [sym]: 1, a: 2 };
  const clonedObj = deepClone(obj);
  expect(clonedObj).toEqual(obj);
  expect(clonedObj[sym]).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

image-20240716175612883

标签:deepClone,const,target,实现,JS,toBe,expect,key,拷贝
From: https://www.cnblogs.com/feixianxing/p/18305861/javascript-deep-clone-and-shallow-clone

相关文章

  • 利用wps的com口用python实现excel转pdf
    因为最近每天都要进行表格相关的工作,每天都要整理数据导出pdf,因为导出的表格格式比较复杂,要求也比较严格,所以python导出pdf的库都满足不了需求,比较好用的又需要付费,最后摸索到了可以用应用的com口完成导出因为微软excel在导出多个sheet时比较大的sheet页并不会缩小内容而是扩大......
  • 代码随想录算法训练营第九天 | 151.翻转字符串里的单词、卡码网:55.右旋转字符串、28.
    151.翻转字符串里的单词题目:.-力扣(LeetCode)思路:用快慢双指针重置空格,先整体翻转再局部翻转代码:classSolution{public:voidremoveSpace(string&s){intslow=0;for(intfast=0;fast<s.size();fast++){if(slow!=0&&s[fast]!='')......
  • Java实现将json数据转换为sql insert语句
    Java实现将json数据转换为sqlinsert语句importcom.fasterxml.jackson.core.JsonProcessingException;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importjava.util.Iterator;importjava.util.Map;publicclassJson......
  • python+flask计算机毕业设计基于Vue.js的付费阅读小程序(程序+开题+论文)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,数字化阅读已成为现代人获取知识、娱乐休闲的重要方式之一。然而,在海量信息面前,如何有效保护知识产权,激励内容创......
  • 新版网页无插件H.265播放器EasyPlayer.js如何测试demo视频?
    H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器,可支持多种流媒体协议播放,支持H.264与H.265编码格式,性能稳定、播放流畅;支持WebSocket-FLV、HTTP-FLV,HLS(m3u8)、WebRTC、WS-FMP4、HTTP-FMP4等格式的视频流,并且已实现网页端实时录像、在iOS上实现低延时......
  • 使用STM32实现智能电子秤
    智能电子秤是一种基于微控制器的测量仪器,能够通过传感器测量物体的重量,并将结果显示在液晶屏上。本文将详细介绍如何使用STM32微控制器实现一个简单的智能电子秤。首先,我们需要准备的硬件设备有:STM32开发板(例如STM32F103C8T6)HX711模块(AD转换芯片)串行LCD模块或OLED模块(用于显......
  • go实现测试端口是否打开
    packagemainimport( "fmt" "net" "time")funccheckUDPPort(addressstring,timeouttime.Duration)(bool,time.Duration){ varconnnet.Conn varerrerror start:=time.Now() conn,err=net.DialTimeout("udp&qu......
  • 通过chrony实现内网自建时间同步服务器
    服务端安装chrony服务端yuminstall-ychrony配置chrony服务端#chrony默认配置文件路径#yum:一般为/etc/chrony.conf#apt:一般为/etc/chrony/chrony.conf#在chrony.conf中加入以下行serverntp.aliyun.comiburstmanualallow0.0.0.0/0localstratum8......
  • CSS 不用JavaScript实现动画 box-shadow 渐变实现内切角
    阴影实现的关键点在于使用伪元素绝对定位在容器的一角,元素本身透明,阴影扩散开形成内切圆角效果阴影实现缺点,单个标签最多只能是2个内切圆角径向渐变实现内切圆角可以是4边HTML:<divclass="shadow">使用阴影的扩散半径实现内切圆角</div><divclass="shadow2">阴影实现缺点......
  • eNSP校园网设计与实现
    概述此次设计,完成了VLAN划分、WLAN、OSPF、VRRP等基本配置,之后出于对企业top图的完整性和网络安全性的考虑,在此基础上引入了防火墙和防火墙双热备,并在防火墙上成功配置了OSPF、NAT、GREVPN、IPsecVPN,成功完成了企业网的基本功能。1.目录第一章需求分析1.1设计背景......