你写的深度克隆真的“深度”吗?

深度克隆是前端开发中无法避免的话题,几乎每个前端开发者都遇到过这个话题,那我们就来看看你写的深度克隆真的正确吗?

大家先看下面这段代码:

1/** 2 * 我是最强的深度克隆 3 */ 4const deepClone = (obj: any) => { 5 return JSON.parse(JSON.stringify(obj)) 6}

平时开发中用这个方法或者过去用过这个方法去“深度克隆”的同学请举手🙋,我相信应该不在少数。也不是说这个方法是错的,它其实在绝大多数场景都能用,但是在一些复杂场景就会有问题,比如下面这几个常见的场景:

  1. 传入的克隆对象中包含循环、递归引用;
  2. 传入的克隆对象中包含不可序列化的数据类型,比如:
  • undefined:会被丢弃;
  • function:会被丢弃;
  • Symbol:会被丢弃;
  • Date:会被转换为字符串;
  • RegExp:会被转换为空对象;
  • Map 和 Set:会被转换为空对象;
  • BigInt:会抛出 TypeError,因为 JSON 不支持 BigInt 类型;
  1. 传入的克隆对象中包含不可枚举的属性;
  2. ……

所以,我们秉承着一个顶级的前端开发者😏的职业素养,肯定是不能让自己的代码出现bug,并且为了更好的理解其原理,我们自己来手写一个通用性非常强的深度克隆函数。 到这里有的同学可能会问了,为什么不直接使用自带的 structuredClone() 呢,那是因为 structuredClone API 是ES2020的规范了,部分浏览器兼容性可能会有问题,为了尽可能避免出现那种极端情况,还是尽可能自己手写比较好,毕竟自己手写的也能应对大多数情况了。下面贴出来 Can I use? 网站的截图。

WX20240705-100229@2x

前言

这里是写给新入门的前端开发同学看的,前端大佬自觉跳过。 首先我们要明白一个问题,那就是 什么是深度克隆?为什么要去进行深度克隆? 下面我来一一解答。

什么是深度克隆?

笼统的讲就是拷贝一个对象以及他嵌套的所有子对象、数组和属性然后形成一个完全独立(和原对象没半毛钱关系)的副本,你对这个副本进行修改都不会影响到原对象的数据,这就叫做深度克隆。

为什么要去进行深度克隆?

  1. 因为在JavaScript中,对象和数组是通过引用传递的。如果直接复制对象的引用,两个变量将指向同一个内存地址,修改其中一个对象将会影响另一个对象。深度克隆可以创建一个完全独立的副本,从而防止这种意外修改。

正片开始

1/** 2 * 深度克隆 3 * @param obj 克隆对象 4 * @param hash 哈希缓存 5 * @returns 克隆结果 6 */ 7const deepClone = <T>(obj: T, hash = new WeakMap()): T => { 8 // 首先,我们需要判断接受的克隆对象是不是符合格式要求,不符合直接打回去 9 if (obj === null || typeof obj !== 'object') { 10 return obj; 11 } 12 13 // 处理循环引用 14 if (hash.has(obj)) { 15 return hash.get(obj); 16 } 17 18 // 对于 Date 和 RegExp 对象,通过各自的构造函数创建新的实例 19 if (obj instanceof Date) { 20 return new Date(obj.getTime()) as T; 21 } 22 23 if (obj instanceof RegExp) { 24 return new RegExp(obj.source, obj.flags) as T; 25 } 26 27 // 对于 Map 和 Set,递归克隆其元素 28 if (obj instanceof Map) { 29 const mapCopy = new Map(); 30 hash.set(obj, mapCopy); 31 obj.forEach((value, key) => { 32 mapCopy.set(key, deepClone(value, hash)); 33 }); 34 return mapCopy as T; 35 } 36 37 if (obj instanceof Set) { 38 const setCopy = new Set(); 39 hash.set(obj, setCopy); 40 obj.forEach(value => { 41 setCopy.add(deepClone(value, hash)); 42 }); 43 return setCopy as T; 44 } 45 46 // 对于 ArrayBuffer 和 TypedArray,创建新的实例并复制其内容 47 if (obj instanceof ArrayBuffer) { 48 return obj.slice(0) as T; 49 } 50 51 if (ArrayBuffer.isView(obj)) { 52 return new (obj.constructor as any)(obj) as T; 53 } 54 55 // 对于 Symbol,通过 Symbol.prototype.valueOf 方法获取其原始值 56 if (typeof obj === 'symbol') { 57 return Object(Symbol.prototype.valueOf.call(obj)) as T; 58 } 59 60 // 处理普通对象和数组,先判断是数组还是对象 61 const objCopy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj)); 62 // 然后使用 WeakMap 跟踪已克隆的对象,避免处理循环引用时进入死循环 63 hash.set(obj, objCopy); 64 // 使用 Reflect.ownKeys 获取对象的所有属性(包括不可枚举属性和 Symbol 属性) 65 // 最后再对于每个属性,递归调用 deepClone 方法 66 Reflect.ownKeys(obj).forEach(key => { 67 (objCopy as any)[key] = deepClone((obj as any)[key], hash); 68 }); 69 70 // 返回结果,完美~ 71 return objCopy as T; 72}

写完之后,我们测试一下

1// 测试示例 2const obj = { 3 a: 1, 4 b: { c: 2 }, 5 d: new Date(), 6 e: /regex/gi, 7 f: new Map([[1, 'one']]), 8 g: new Set([1, 2, 3]), 9 h: new Uint8Array([1, 2, 3]), 10 i: Symbol('sym'), 11 j: null, 12 k: undefined, 13 l: function() { console.log('hello'); }, 14 m: [1, 2, { n: 3 }], 15}; 16 17const result = deepClone(obj); 18console.log('result', result);

使用 ts-node 运行一下看看:

d06456290f444a4febe2f7ef90ee0053

OK,完美克隆,下面将繁杂的注释语句去掉,复制到自己代码里去食用吧~

1/** 2 * 深度克隆 3 * @param obj 克隆对象 4 * @param hash 哈希缓存 5 * @returns 克隆结果 6 */ 7const deepClone = <T>(obj: T, hash = new WeakMap()): T => { 8 // 处理 null 或非对象 9 if (obj === null || typeof obj !== 'object') { 10 return obj; 11 } 12 13 // 处理循环引用 14 if (hash.has(obj)) { 15 return hash.get(obj); 16 } 17 18 // 处理 Date 19 if (obj instanceof Date) { 20 return new Date(obj.getTime()) as T; 21 } 22 23 // 处理 RegExp 24 if (obj instanceof RegExp) { 25 return new RegExp(obj.source, obj.flags) as T; 26 } 27 28 // 处理 Map 29 if (obj instanceof Map) { 30 const mapCopy = new Map(); 31 hash.set(obj, mapCopy); 32 obj.forEach((value, key) => { 33 mapCopy.set(key, deepClone(value, hash)); 34 }); 35 return mapCopy as T; 36 } 37 38 // 处理 Set 39 if (obj instanceof Set) { 40 const setCopy = new Set(); 41 hash.set(obj, setCopy); 42 obj.forEach(value => { 43 setCopy.add(deepClone(value, hash)); 44 }); 45 return setCopy as T; 46 } 47 48 // 处理 ArrayBuffer 49 if (obj instanceof ArrayBuffer) { 50 return obj.slice(0) as T; 51 } 52 53 // 处理 TypedArray 54 if (ArrayBuffer.isView(obj)) { 55 return new (obj.constructor as any)(obj) as T; 56 } 57 58 // 处理 Symbol 59 if (typeof obj === 'symbol') { 60 return Object(Symbol.prototype.valueOf.call(obj)) as T; 61 } 62 63 // 处理普通对象和数组 64 const objCopy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj)); 65 hash.set(obj, objCopy); 66 Reflect.ownKeys(obj).forEach(key => { 67 (objCopy as any)[key] = deepClone((obj as any)[key], hash); 68 }); 69 70 return objCopy as T; 71}
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

PPDB:今晚老齐直播

【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。

VBox 启动虚拟机失败

在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern