js深拷贝deepCopy教程:支持循环引用、类型不丢失、可扩展、可定制

目录

  • 1. 背景
  • 2. 简介
  • 3. 安装方式
    • 3.1. 方式1:通过 npm 安装
    • 3.2. 方式2:直接下载原代码
    • 3.3. 方式3:通过
    • 使用全局的 deepCopy

      
      
    • 4. 教程

      4.1. API简介

      deep-copy 导出了两个工具函数

      import {deepCopy,createDeepCopy} from "deep-copy"
      
      • deepCopy(value:V,options?:DeepCopyOptions|null|undefined,typeCopyers?:TypeRevivers|null|undefined):V:对 value 进行深拷贝操作,并将深拷贝后的值返回;参数如下:

        • value: any:被拷贝的值;
        • options?:DeepCopyOptions|null|undefined:描述拷贝配置的选项对象;
        • typeCopyers?:TypeRevivers|null|undefined:可根据不同类型定制拷贝逻辑的配置对象
      • createDeepCopy(presetTypeCopierMap?:TypeReviverMap):DeepCopy:创建并返回带有默认 typeCopyers 的 deepCopy() 函数;参数如下:

        • presetTypeCopierMap?:TypeReviverMap:默认的 typeCopyers;

      4.2. 基本使用

      deep-copy 库中 会导出 deepCopy() 函数,用它可以直接对值进行深拷贝,如下所示:

      import {deepCopy} from "../dist/deep-copy.es"
      
      let value = {
          name:"root",
          sub:{
              name:"member1",
              fun:function () {
                  console.log("这是函数")
              }
          }
      };
      
      let copy = deepCopy(value);
      

      其中 copy 和 value 的信息结构是完全一样的,但 copy 和 value 及其成员(除了函数(方法)外)已经是完全两份实例,占据着两份不同的内存空间;

      4.3. 拷贝函数

      默认情况下,deepCopy() 不会对 函数对象 进行深拷贝,比如上例中的 value.sub.fun,所以,copy 中的函数(方法) 和 value 中的函数 是同一函数,占据同一份内存空间,即 copy.sub.fun === value.sub.fun

      如果希望 deepCopy() 对函数也进行深拷贝,可以给 deepCopy() 传递第2个参数并设置选项 copyFuntrue,如下所示:

      let copy = deepCopy(value,{
          copyFun:true
      });
      

      些时,copy 中的函数(方法) 和 value 中的函数 具有相同的代码逻辑,但已经是两个不同的函数实例,占据不同的内存空间,即 copy.sub.fun !== value.sub.fun

      4.4. 指定拷贝深度

      如果你只需要实例进行一定深度的深拷贝,比如:只对实例、实例的成员、实例的成员的成员 进行拷贝操作,再深入的不进行拷贝操作,像这样的需求,可以通过给 deepCopy 传递 maxDepth 选项来实现,如下:

      let copy = deepCopy(value,{
          maxDepth:2
      });
      

      maxDepth 是可选的,默认值为:Infinity;表示拷贝的最大深度;当值为 undefinednull 时,会使用默认值,表示无限深度;被拷贝的值(本例中是 value)本身的深度为 0 ,被拷贝值的成员的深度为 1 ,依次类推;

      4.5. 循环引用

      当对象 与 对象 之间 或 对象 与 成员 之前 存在类似以下引用关系时,就会形成循环引用


      循环引用

      JSON的深拷贝方案是无法处理循环引用关系 let copy = JSON.parse(JSON.stringify(value)),会造成内存溢出;

      deepCopy() 支持拷贝这种带循环引用关系的对象,并且拷贝后的值,仍然会保持这种循环引用关系,如下所示:

      let root = {
          name:"root"
      };
      
      let member1 = {
          name:"member1",
          fun:function () {
              console.log("这是函数")
          }
      };
      
      
      let member2 = {
          name:"member2",
      };
      
      root.sub = member1;
      member1.sub = member2;
      member2.sub = root;
      
      let rootCopy = DC.deepCopy(root);
      

      拷贝前 和 拷贝后的 引用关系保持一样:

      root.sub.sub.sub === root;  // true
      rootCopy.sub.sub.sub === rootCopy;  // true
      

      4.6. 保持类型信息

      有些时候,被拷贝的对象并不是普通的对象,它可能是某个类的实例,比如:

      class Person {
          constructor(){
              this.name = "";
              this.email = "";
          }
      }
      
      let p = new Person();
      p.name = "郭斌勇";
      p.email = "[email protected]";
      

      p 是 类 Person 的实例,但通过 JSON方案拷贝后

      let pCopy = JSON.parse(JSON.stringify(p));
      
      pCopy instanceof Person; // false
      pCopy instanceof Object; // true
      

      pCopy 却变成了普通对象,即 Object 类型的实例;

      然而,deepCopy() 会保持这种类型信息,如下:

      let pCopy = deepCopy(p);
      pCopy instanceof Person;  // true
      

      拷贝后的 pCopy 仍然是 Person 类型的实例;

      注意:
      deep-copy 在拷贝实例时,会用实例所属的类创建一个新的实例,创建时并不会给构造函数传递任何参数, 然后将原来实例本身的成员的副本重设 新实例为新实例的成员;这对于那些需要给构造函数传递参数的类 可能会存在问题;如果某类创建实例时依赖构造函数的参数,则您可以针对些类定制拷贝规则;

      4.7. 拷贝不可枚举的属性

      deepCopy() 默认只拷贝对象自身的可枚举属性,如果您也想拷贝对象自身的不可枚举的属性,则可以给 deepCopy() 传递 allOwnProps 选项,并设置为 true,如下:

      let copy = deepCopy(value,{
          allOwnProps:true
      });
      
      • allOwnProps?:boolean | undefined | null:可选选项;默认值: undefined; 表示是否要拷贝所有自身的属性,包不可枚举的,但不包括原型链上的属性;
        • true:拷贝对象自身(不包括原型上的)的所有属性(包括不可枚举的);
        • false|undefined|null : 只拷贝对象自身中(不包括原型上的)可枚举的属性;

      4.8. 自定义拷贝规则

      对于下面这个 Person 类,在创建实例时(比如:p)需要传入一个构造参数 sex,这个参数决定了模型的构建过程,如下:

      class Person {
          constructor(sex){
              this.sex = sex;
              // 根据 sex  初始化模型
              switch(sex){
                  case "男":{
                      console.log("初始化男性特性");
                      break;
                  }
                  case "女":{
                      console.log("初始化女性特性");
                      break;
                  }
                  default:{
                      console.log("初始化人妖特性");
                  }
              }
      
              this.name = "";
              this.email = "";
              this.idCard = null; //身份证,是一个对象
              this.mate = null;  //配偶,也是 Person 类的实例
              
          }
      }
      
      
      let p = new Person("男");
      p.name = "郭斌勇";
      p.email = "[email protected]";
      p.idCard = {
          id:4231232776886677,  //身份
          address:"中国的一个小农村里"
      };
      
      //设置配偶
      p.mate = new Person("女");
      p.mate.name = "简爱";
      

      像这种类型的类,如果使用 deepCopy() 的默认拷贝逻辑的话,新拷贝的 Person 实例可能会执行错误的构建过程,因为 deepCopy() 在创建新实例时,不会传入任何构建参数;像这种类型,我们就需要提供自定义的拷贝规则了;

      如果你想自定义某个类型实例的深拷贝规则,有以下几种方案:

      • deepCopy() 函数提供 typeCopyers 参数 或 设置 presetTypeCopierMap 预设;
      • 给实例增加 getCopy 方法;

      4.8.1. typeCopyers

      可以通过 typeCopyers 参数自定义拷贝规则,如下:

      let pCopy = deepCopy(p,null,{
          // 当需要拷贝 Person 类型的实例时,会回调这个函数,这个函数需要返回 Person 实例的副本;这个函数称为 拷贝者 Copier
          "Person": function(value,copyMember,options){
              let copy = new Person(value.sex);  // 创建新的 Person 实例,作为 value 的副本
      
              // 给副本 copy 设置基本类型的成员;
              copy.name = value.name;
              copy.email = value.email;
      
              // 如果引用类型的成员也需要深拷贝,则需要通过 copyMember() 函数拷贝 引用类型的成员
              copy.idCard = copyMember(value.idCard);
              /**
               * copyMember() 函数会返回 拷贝后的值;
               * 但如果被拷贝的值存在循环引用的话,copyMember() 会返回 undefined;这种情况下,可通过给 copyMember() 传递回调函数(第2个参数),通过回调函数来获取拷贝后的值;
               * 无论是否存在循环引用,通过回调函数总能拿到拷贝后的值,回调函数是一种保险的方法
               */
              copyMember(value.mate,function(mateCopy){
                  copy.mate = mateCopy;
              });
      
              // 需要将副本返回;
              return copy;
          }
      });
      

      deepCopy() 函数的 typeCopyers 参数是 TypeRevivers 类型的,是用来描述 类型 和 拷贝者 Copier(提供拷贝实例回调函数)的对应关系的,上例中 typeCopyers 使用的是 对象形式,它共以下几种描述方式:

      • {[TypeName:ExactTypeName]:Copyer}:对象形式,属性名字 是 类型的名字,属性值是类型的 拷贝者(Copyer);
      • [Types, Copyer][]:数组形式,数组中的元素是 [Types, Copyer] 类型的元组,元组的第一个元素是 Types,第二个元素是 拷贝者;
      • Map:Map形式,Map中的键是 Types 类型,值是 拷贝者;

      4.8.1.1. Types

      TypesExactTypeNameExactType 和 其数组结构的联合,定义如下:

      type Types = DataType | DataTypeArray;
      type DataType = ExactTypeName | ExactType;
      type DataTypeArray = DataType[];
      
      • ExactType:精确类型,可更加细致地描述类型;各种类型的值(左侧) 与 ExactType(右侧) 的映射如下:

        • undefinedundefined
        • null : null
        • string : "string"
        • number : "number"
        • bigint : "bigint"
        • boolean : "boolean"
        • symbol : "symbol"
        • 普通函数 : Function
        • 异步函数 : AsyncFunction
        • 生成器函数 : GeneratorFunction
        • 没有原型的对象(如:通过 Object.create(null) 创建的对象) : "object"
        • 其它任何类型的实例 : 该实例的构造函数
      • ExactTypeName:精确类型的字符串表示;各种类型的值(左侧) 与 ExactTypeName(右侧) 的映射如下:

        • undefined"undefined"
        • null : "null"
        • string : "string"
        • number : "number"
        • bigint : "bigint"
        • boolean : "boolean"
        • symbol : "symbol"
        • 普通函数 : "Function"
        • 异步函数 : "AsyncFunction"
        • 生成器函数 : "GeneratorFunction"
        • 没有原型的对象(如:通过 Object.create(null) 创建的对象) : "object"
        • 其它任何类型的实例 : 该实例的构造函数的名字

      所以,Types 可以是具体的类型 ExactType ,比如:Person 类;也可以是 类型的名字字符串 ExactTypeName ,比如:'Person';或者是包含多个类型的数组,比如:[Person,'Map',Set]

      4.8.1.2. 拷贝者Copyer

      拷贝者 Copyer 是一个类型为 (value:T,copyMember:CopyMember,options:CopierOptions)=>T 函数,严格的定义如下:

      type Copier = (this:T,value:T,copyMember:CopyMember,options:CopierOptions)=>T
      

      当需要拷贝某个类型的实例时,就会调用这个类型的 拷贝者 Copyer ,然后将拷贝者返回的值作为那个实例的副本;

      在调用拷贝者 Copyer 时,会将 Copyer 的 this 设置为被拷贝的值(与 value 参数相同的值),并会给 Copyer 传递以下参数:

      • value:被拷贝的值;
      • copyMember:用于深拷贝 value 的成员的深拷贝函数;类型为 (member:T,completeCB?:CompleteCB|null|undefined,key?:K,host?:H | null | undefined,options?:CopyMemberOptions)=>T|undefined;拷贝后副本会通过 copyMember() 的返回值(当被拷贝的成员不存在循环引用时) 和 completeCB 回调函数 返回;
      • options:包含了其它信息的对象
         {
            allOwnProps:boolean;
            key:any;
            host:Host;
            type:string;
            depth:number;
            copyFun:boolean;
        }
        

      拷贝者 Copyer 需要将自定创建的 value 的副本返回;

      在创建 value 的副本的过程中,如果需要对 value 的引用类型的成员 进行拷贝,则需要调用 copyMember(value) 函数,这个函数是专门用于 深拷贝 value 的成员的;拷贝后副本会通过 copyMember(value) 的返回值(当被拷贝的成员不存在循环引用时) 和 传给 copyMember(value,completeCB)completeCB 回调函数 传回;例外的情况是:如果 value 的某个成员存在循环引用,则 copyMember(value) 会返回 undefined,这种情况下,只能通过 completeCB 回调函数 传回拷贝后的值;

      4.8.2. 预设presetTypeCopierMap

      如果经常需要给 deepCopy() 传递包含相同 类型和拷贝者 的 typeCopyers 参数,则可以将那些经常用到的 拷贝者 设置到 deepCopy() 的预设 deepCopy.presetTypeCopierMap 中;

      deepCopy 函数对象有个属性 presetTypeCopierMap 是用来设置常用的拷贝者的,它的类型是 Map,所以上面的例子也可以如下设置预设:

      // 当需要拷贝 Person 类型的实例时,会回调些函数
      deepCopy.presetTypeCopierMap.set(Person,function(value,copyMember,options){
          let copy = new Person(value.sex);  // 创建新的 Person 实例,作为 value 的副本
      
          // 给副本 copy 设置基本类型的成员;
          copy.name = value.name;
          copy.email = value.email;
      
          // 如果引用类型的成员也需要深拷贝,则需要通过 copyMember() 函数拷贝 引用类型的成员
          copy.idCard = copyMember(value.idCard);
          /**
           * copyMember() 函数会返回 拷贝后的值;
           * 但如果被拷贝的值存在循环引用的话,copyMember() 会返回 undefined;这种情况下,可通过给 copyMember() 传递回调函数(第2个参数),通过回调函数来获取拷贝后的值;
           * 无论是否存在循环引用,通过回调函数总能拿到拷贝后的值,回调函数是一种保险的方法
           */
          copyMember(value.mate,function(mateCopy){
              copy.mate = mateCopy;
          });
      
          // 需要将副本返回;
          return copy;
      })
      

      以后使用 deepCopy() 时,就不需要传递 Person 类型的拷贝者了;

      4.8.3. 拷贝者的优先级

      当需要拷贝某个类型的实例时,deepCopy() 会通过以下几种方法获取该实例的副本:

      • 首先使用 typeCopyers 参数中的拷贝者;
      • 如果没有找到拷贝者,则再使用预设 presetTypeCopierMap 中的拷贝者;
      • 如果没有找到拷贝者,则会将实例的 getCopy 方法作为拷贝者;
      • 如果实例没有 getCopy 方法,则会使用 typeCopyers 参数中 "default" 对应的 拷贝者;
      • 如果没有找到拷贝者,则会使用预设 presetTypeCopierMap"default" 对应的 拷贝者;
      • 如果没有找到拷贝者,则会使用内置的默认拷贝逻辑;

      其中,"default" 对应的拷贝者称为 默认的拷贝者;

      4.8.4. getCopy

      通过上文的 [拷贝者的优先级][] 可以知道,我们也可以给对象增加 getCopy 方法 来自定义 该对象的拷贝逻辑,如果您想将拷贝者应用到某个类型的所有实例上,则只需要给该类型增加一个实例方法 getCopy 即可;

      getCopy 方法的 和 拷贝者 Copier 的类型一样,都是类型为 (value:T,copyMember:CopyMember,options:CopierOptions)=>T 函数;

      所以,上例中,也可以如下自定义 实例 p 的拷贝逻辑:

      p.getCopy = function(value,copyMember,options){
          let copy = new Person(value.sex);  // 创建新的 Person 实例,作为 value 的副本
      
          // 给副本 copy 设置基本类型的成员;
          copy.name = value.name;
          copy.email = value.email;
      
          // 如果引用类型的成员也需要深拷贝,则需要通过 copyMember() 函数拷贝 引用类型的成员
          copy.idCard = copyMember(value.idCard);
          /**
           * copyMember() 函数会返回 拷贝后的值;
           * 但如果被拷贝的值存在循环引用的话,copyMember() 会返回 undefined;这种情况下,可通过给 copyMember() 传递回调函数(第2个参数),通过回调函数来获取拷贝后的值;
           * 无论是否存在循环引用,通过回调函数总能拿到拷贝后的值,回调函数是一种保险的方法
           */
          copyMember(value.mate,function(mateCopy){
              copy.mate = mateCopy;
          });
      
          // 需要将副本返回;
          return copy;
      };
      

      或,如下自定义 Person 类型 的拷贝逻辑:

      Person.prototype.getCopy = function(value,copyMember,options){
          let copy = new Person(value.sex);  // 创建新的 Person 实例,作为 value 的副本
      
          // 给副本 copy 设置基本类型的成员;
          copy.name = value.name;
          copy.email = value.email;
      
          // 如果引用类型的成员也需要深拷贝,则需要通过 copyMember() 函数拷贝 引用类型的成员
          copy.idCard = copyMember(value.idCard);
          /**
           * copyMember() 函数会返回 拷贝后的值;
           * 但如果被拷贝的值存在循环引用的话,copyMember() 会返回 undefined;这种情况下,可通过给 copyMember() 传递回调函数(第2个参数),通过回调函数来获取拷贝后的值;
           * 无论是否存在循环引用,通过回调函数总能拿到拷贝后的值,回调函数是一种保险的方法
           */
          copyMember(value.mate,function(mateCopy){
              copy.mate = mateCopy;
          });
      
          // 需要将副本返回;
          return copy;
      };
      

      4.9. 创建带预设拷贝规则的拷贝函数

      如果您在使用 deepCopy() 时,经常需要使用不同的预设,那您可能需要使用 createDeepCopy(presetTypeCopierMap) 来创建一个带有预设 presetTypeCopierMap 的另一个新的 deepCopy() 函数;

      createDeepCopy(presetTypeCopierMap) 方法会创建一个新的深拷贝函数,这个新的深拷贝函数 和 deepCopy() 具有相同的功能,只是 不同的对象,且预设为传给 createDeepCopy(presetTypeCopierMap) 函数的 presetTypeCopierMap 参数,如下:

      const presetTypeCopierMap = new Map();
      presetTypeCopierMap.set(Person,function(value,copyMember,options){
          let copy = new Person(value.sex);  // 创建新的 Person 实例,作为 value 的副本
      
          // 给副本 copy 设置基本类型的成员;
          copy.name = value.name;
          copy.email = value.email;
      
          // 如果引用类型的成员也需要深拷贝,则需要通过 copyMember() 函数拷贝 引用类型的成员
          copy.idCard = copyMember(value.idCard);
          /**
           * copyMember() 函数会返回 拷贝后的值;
           * 但如果被拷贝的值存在循环引用的话,copyMember() 会返回 undefined;这种情况下,可通过给 copyMember() 传递回调函数(第2个参数),通过回调函数来获取拷贝后的值;
           * 无论是否存在循环引用,通过回调函数总能拿到拷贝后的值,回调函数是一种保险的方法
           */
          copyMember(value.mate,function(mateCopy){
              copy.mate = mateCopy;
          });
      
          // 需要将副本返回;
          return copy;
      });
      
      // deepCopy2 为新的深拷贝函数,功能和 deepCopy 一样;
      const deepCopy2 = createDeepCopy(presetTypeCopierMap);
      

      其中 deepCopy2 为新的深拷贝函数,功能和 deepCopy 一样;通过 deepCopy2 来进行深拷贝操作,如下:

      let copy = deepCopy2(value);
      

你可能感兴趣的:(js深拷贝deepCopy教程:支持循环引用、类型不丢失、可扩展、可定制)