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

本文由 ChatMoney团队出品

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

大家先看下面这段代码:

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

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

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

所以,我们秉承着一个顶级的前端开发者😏的职业素养,肯定是不能让自己的代码出现bug,并且为了更好的理解其原理,我们自己来手写一个通用性非常强的深度克隆函数。

前言

这里是写给新入门的前端开发同学看的,前端大佬自觉跳过。

首先我们要明白一个问题,那就是什么是深度克隆?为什么要去进行深度克隆?下面我来一一解答。

什么是深度克隆

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

为什么要去进行深度克隆

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

正片开始

/**
 * 深度克隆
 * @param obj 克隆对象
 * @param hash 缓存
 * @returns 克隆结果
 */
const deepClone = <T>(obj: T, hash = new WeakMap()): T => {
    // 首先,我们需要判断接受的克隆对象是不是符合格式要求,不符合直接打回去
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // 处理循环引用
    if (hash.has(obj)) {
        return hash.get(obj);
    }

    // 对于 Date 和 RegExp 对象,通过各自的构造函数创建新的实例
    if (obj instanceof Date) {
        return new Date(obj.getTime()) as T;
    }

    if (obj instanceof RegExp) {
        return new RegExp(obj.source, obj.flags) as T;
    }

    // 对于 Map 和 Set,递归克隆其元素
    if (obj instanceof Map) {
        const mapCopy = new Map();
        hash.set(obj, mapCopy);
        obj.forEach((value, key) => {
            mapCopy.set(key, deepClone(value, hash));
        });
        return mapCopy as T;
    }

    if (obj instanceof Set) {
        const setCopy = new Set();
        hash.set(obj, setCopy);
        obj.forEach(value => {
            setCopy.add(deepClone(value, hash));
        });
        return setCopy as T;
    }

    // 对于 ArrayBuffer 和 TypedArray,创建新的实例并复制其内容
    if (obj instanceof ArrayBuffer) {
        return obj.slice(0) as T;
    }

    if (ArrayBuffer.isView(obj)) {
        return new (obj.constructor as any)(obj) as T;
    }

    // 对于 Symbol,通过 Symbol.prototype.valueOf 方法获取其原始值
    if (typeof obj === 'symbol') {
        return Object(Symbol.prototype.valueOf.call(obj)) as T;
    }

    // 处理普通对象和数组,先判断是数组还是对象
    const objCopy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
    // 然后使用 WeakMap 跟踪已克隆的对象,避免处理循环引用时进入死循环
    hash.set(obj, objCopy);
    // 使用 Reflect.ownKeys 获取对象的所有属性(包括不可枚举属性和 Symbol 属性)
    // 最后再对于每个属性,递归调用 deepClone 方法
    Reflect.ownKeys(obj).forEach(key => {
        (objCopy as any)[key] = deepClone((obj as any)[key], hash);
    });

    // 返回结果,完美~
    return objCopy as T;
}

写完之后,我们测试一下

// 测试示例
const obj = {
    a: 1,
    b: { c: 2 },
    d: new Date(),
    e: /regex/gi,
    f: new Map([[1, 'one']]),
    g: new Set([1, 2, 3]),
    h: new Uint8Array([1, 2, 3]),
    i: Symbol('sym'),
    j: null,
    k: undefined,
    l: function() { console.log('hello'); },
    m: [1, 2, { n: 3 }],
};

const result = deepClone(obj);
console.log('result', result);

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

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

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

/**
 * 深度克隆
 * @param obj 克隆对象
 * @param hash 缓存
 * @returns 克隆结果
 */
const deepClone = <T>(obj: T, hash = new WeakMap()): T => {
    // 处理 null 或非对象
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // 处理循环引用
    if (hash.has(obj)) {
        return hash.get(obj);
    }

    // 处理 Date
    if (obj instanceof Date) {
        return new Date(obj.getTime()) as T;
    }

    // 处理 RegExp
    if (obj instanceof RegExp) {
        return new RegExp(obj.source, obj.flags) as T;
    }

    // 处理 Map
    if (obj instanceof Map) {
        const mapCopy = new Map();
        hash.set(obj, mapCopy);
        obj.forEach((value, key) => {
            mapCopy.set(key, deepClone(value, hash));
        });
        return mapCopy as T;
    }

    // 处理 Set
    if (obj instanceof Set) {
        const setCopy = new Set();
        hash.set(obj, setCopy);
        obj.forEach(value => {
            setCopy.add(deepClone(value, hash));
        });
        return setCopy as T;
    }

    // 处理 ArrayBuffer
    if (obj instanceof ArrayBuffer) {
        return obj.slice(0) as T;
    }

    // 处理 TypedArray
    if (ArrayBuffer.isView(obj)) {
        return new (obj.constructor as any)(obj) as T;
    }

    // 处理 Symbol
    if (typeof obj === 'symbol') {
        return Object(Symbol.prototype.valueOf.call(obj)) as T;
    }

    // 处理普通对象和数组
    const objCopy = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
    hash.set(obj, objCopy);
    Reflect.ownKeys(obj).forEach(key => {
        (objCopy as any)[key] = deepClone((obj as any)[key], hash);
    });

    return objCopy as T;
}

关于我们

本文由ChatMoney团队出品,ChatMoney专注于AI应用落地与变现,我们提供全套、持续更新的AI源码系统与可执行的变现方案,致力于帮助更多人利用AI来变现,欢迎进入ChatMoney获取更多AI变现方案!

ChatMoney的头像ChatMoney
Previous 2024年 7月 31日 下午2:55
Next 2024年 8月 2日 上午11:57

相关推荐

  • PHP数据结构之栈

    本文由 ChatMoney团队出品 栈(Stack)是一种后进先出(Last In First Out, LIFO)的数据结构,它只允许在一端(称为栈顶)进行插入和删除操作。栈的应用非常广泛,例如在编程语言的函数调用中,每次函数调用都会将一个新的帧压入栈中,当函数返回时,该帧会被弹出。此外,栈还常用于解决某些算法问题,如括号匹配、深度优先搜索等。 栈的基本概…

    2024年 7月 8日
    440
  • 时下最火的绘画应用:Midjourney和Diffusion有何区别

    本文由 ChatMoney团队出品 Midjourney与Stable Diffusion:对比分析 1. 易用性与部署 Midjourney: 在线操作:Midjourney的最大优势在于其无需下载,直接在线操作的特点。这使得用户可以轻松上手,无需担心硬件性能问题。 简单学习:由于其网页操作的特性,Midjourney的学习和使用都相对简单,适合快速生成创…

    2024年 6月 28日
    235
  • PHP的命名空间

    本文由 ChatMoney团队出品 PHP 命名空间:模块化和避免命名冲突 在 PHP 项目中,命名空间用于对代码进行模块化和避免命名冲突,尤其在大型项目或使用第三方库时尤为重要。本文将介绍如何使用 PHP 命名空间来组织你的代码。 什么是命名空间 命名空间是通过 namespace 关键字定义的。它们提供了一种方法来封装一组相关的类、接口、函数和常量,从而…

    2024年 7月 31日
    190
  • TypeScript中,如何利用数组生成一个联合类型

    本文由 ChatMoney团队出品 在开发中我们常常会遇到这样一个问题,代码如下: 我们想要传入一个参数到str,而且这个参数必须是arr数组中的某一个元素,这时我们希望的是可以直接得到这个arr的联合类型,接下来一般我们会使用传统的方法去声明类型,如下: 先不说这样的写法很笨,写的时候就已经很ex了,我们希望的是Strs可以根据上面arr的值来自动生成一个…

    2024年 7月 2日
    184
  • 面向对象设计基本原则

    本文由 ChatMoney团队出品 引言 在软件开发过程中,随着系统复杂度的增加和业务变更,程序员面临诸多挑战,如耦合性、内聚性、可维护性、可扩展性和可重用性。设计模式能有效地解决这些问题。设计模式蕴含了面向对象的精髓,掌握面向对象设计和分析是掌握设计模式的基础。它能帮助我们优化代码结构,提高代码的可维护性、可扩展性和可读性。 设计模式遵循一定的原则,这些原…

    2024年 8月 5日
    233

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信