UNPKG

@gby/deep-copy

Version:

deep-copy 深拷贝,可对任意数据进行深度拷贝,包括 函数 function、正则 RegExp、Map、Set、Date、Array、URL 等等;支持含循环引用对象的拷贝,并且不会丢失成员的引用关系 和 类型信息,支持扩展,可根据数据类型定制拷贝逻辑,也可指定拷贝深度;所以,通过它可实现对任意类型的数据进行任意想要的拷贝

463 lines (367 loc) 20.7 kB
[循环引用]: ./assets/循环引用.svg 目录 ======= <!-- TOC --> - [1. API简介](#1-api简介) - [2. 基本使用](#2-基本使用) - [3. 拷贝函数](#3-拷贝函数) - [4. 指定拷贝深度](#4-指定拷贝深度) - [5. 循环引用](#5-循环引用) - [6. 保持类型信息](#6-保持类型信息) - [7. 拷贝不可枚举的属性](#7-拷贝不可枚举的属性) - [8. 自定义拷贝规则](#8-自定义拷贝规则) - [8.1. typeCopyers](#81-typecopyers) - [8.1.1. Types](#811-types) - [8.1.2. 拷贝者Copyer](#812-拷贝者copyer) - [8.2. 预设presetTypeCopierMap](#82-预设presettypecopiermap) - [8.3. 拷贝者的优先级](#83-拷贝者的优先级) - [8.4. getCopy](#84-getcopy) - [9. 创建带预设拷贝规则的拷贝函数](#9-创建带预设拷贝规则的拷贝函数) <!-- /TOC --> 内容 ======== # 1. API简介 deep-copy 导出了两个工具函数 ``` import {deepCopy,createDeepCopy} from "deep-copy" ``` + `deepCopy<V>(value:V,options?:DeepCopyOptions|null|undefined,typeCopyers?:TypeRevivers<Copier>|null|undefined):V`:对 value 进行深拷贝操作,并将深拷贝后的值返回;参数如下: - `value: any`:被拷贝的值; - `options?:DeepCopyOptions|null|undefined`:描述拷贝配置的选项对象; - `typeCopyers?:TypeRevivers<Copier>|null|undefined`:可根据不同类型定制拷贝逻辑的配置对象 + `createDeepCopy(presetTypeCopierMap?:TypeReviverMap<Copier>):DeepCopy`:创建并返回带有默认 typeCopyers 的 `deepCopy()` 函数;参数如下: - `presetTypeCopierMap?:TypeReviverMap<Copier>`:默认的 typeCopyers; # 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 及其成员(除了函数(方法)外)已经是完全两份实例,占据着两份不同的内存空间; # 3. 拷贝函数 默认情况下,`deepCopy()` 不会对 函数对象 进行深拷贝,比如上例中的 `value.sub.fun`,所以,`copy` 中的函数(方法) 和 `value` 中的函数 是同一函数,占据同一份内存空间,即 `copy.sub.fun === value.sub.fun`; 如果希望 `deepCopy()` 对函数也进行深拷贝,可以给 `deepCopy()` 传递第2个参数并设置选项 `copyFun``true`,如下所示: ``` let copy = deepCopy(value,{ copyFun:true }); ``` 些时,`copy` 中的函数(方法) 和 `value` 中的函数 具有相同的代码逻辑,但已经是两个不同的函数实例,占据不同的内存空间,即 `copy.sub.fun !== value.sub.fun`# 4. 指定拷贝深度 如果你只需要实例进行一定深度的深拷贝,比如:只对实例、实例的成员、实例的成员的成员 进行拷贝操作,再深入的不进行拷贝操作,像这样的需求,可以通过给 `deepCopy` 传递 `maxDepth` 选项来实现,如下: ``` let copy = deepCopy(value,{ maxDepth:2 }); ``` `maxDepth` 是可选的,默认值为:`Infinity`;表示拷贝的最大深度;当值为 `undefined``null` 时,会使用默认值,表示无限深度;被拷贝的值(本例中是 `value`)本身的深度为 0 ,被拷贝值的成员的深度为 1 ,依次类推; # 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 ``` # 6. 保持类型信息 有些时候,被拷贝的对象并不是普通的对象,它可能是某个类的实例,比如: ``` class Person { constructor(){ this.name = ""; this.email = ""; } } let p = new Person(); p.name = "郭斌勇"; p.email = "guobinyong@qq.com"; ``` `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 在拷贝实例时,会用实例所属的类创建一个新的实例,创建时并不会给构造函数传递任何参数, 然后将原来实例本身的成员的副本重设 新实例为新实例的成员;这对于那些需要给构造函数传递参数的类 可能会存在问题;如果某类创建实例时依赖构造函数的参数,则您可以针对些类定制拷贝规则; # 7. 拷贝不可枚举的属性 `deepCopy()` 默认只拷贝对象自身的可枚举属性,如果您也想拷贝对象自身的不可枚举的属性,则可以给 `deepCopy()` 传递 `allOwnProps` 选项,并设置为 `true`,如下: ``` let copy = deepCopy(value,{ allOwnProps:true }); ``` + `allOwnProps?:boolean | undefined | null `:可选选项;默认值: `undefined`; 表示是否要拷贝所有自身的属性,包不可枚举的,但不包括原型链上的属性; - `true`:拷贝对象自身(不包括原型上的)的所有属性(包括不可枚举的); - `false|undefined|null` : 只拷贝对象自身中(不包括原型上的)可枚举的属性; # 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 = "guobinyong@qq.com"; p.idCard = { id:4231232776886677, //身份 address:"中国的一个小农村里" }; //设置配偶 p.mate = new Person("女"); p.mate.name = "简爱"; ``` 像这种类型的类,如果使用 `deepCopy()` 的默认拷贝逻辑的话,新拷贝的 `Person` 实例可能会执行错误的构建过程,因为 `deepCopy()` 在创建新实例时,不会传入任何构建参数;像这种类型,我们就需要提供自定义的拷贝规则了; 如果你想自定义某个类型实例的深拷贝规则,有以下几种方案: -`deepCopy()` 函数提供 `typeCopyers` 参数 或 设置 `presetTypeCopierMap` 预设; - 给实例增加 `getCopy` 方法; ## 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>` 类型的,是用来描述 类型 和 拷贝者 Copier(提供拷贝实例回调函数)的对应关系的,上例中 `typeCopyers` 使用的是 对象形式,它共以下几种描述方式: - `{[TypeName:ExactTypeName]:Copyer}`:对象形式,属性名字 是 类型的名字,属性值是类型的 拷贝者(Copyer); - `[Types, Copyer][]`:数组形式,数组中的元素是 `[Types, Copyer]` 类型的元组,元组的第一个元素是 `Types`,第二个元素是 拷贝者; - `Map<Types, Copyer>`:Map形式,Map中的键是 `Types` 类型,值是 拷贝者; ### 8.1.1. Types `Types``ExactTypeName``ExactType` 和 其数组结构的联合,定义如下: ``` type Types = DataType | DataTypeArray; type DataType = ExactTypeName | ExactType; type DataTypeArray = DataType[]; ``` - `ExactType`:精确类型,可更加细致地描述类型;各种类型的值(左侧) 与 `ExactType`(右侧) 的映射如下: * `undefined``undefined` * `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]`### 8.1.2. 拷贝者Copyer 拷贝者 Copyer 是一个类型为 `(value:T,copyMember:CopyMember,options:CopierOptions)=>T` 函数,严格的定义如下: ``` type Copier<T = any,Host = any> = (this:T,value:T,copyMember:CopyMember,options:CopierOptions<Host>)=>T ``` 当需要拷贝某个类型的实例时,就会调用这个类型的 拷贝者 Copyer ,然后将拷贝者返回的值作为那个实例的副本; 在调用拷贝者 Copyer 时,会将 Copyer 的 `this` 设置为被拷贝的值(与 `value` 参数相同的值),并会给 Copyer 传递以下参数: - `value`:被拷贝的值; - `copyMember`:用于深拷贝 `value` 的成员的深拷贝函数;类型为 `(member:T,completeCB?:CompleteCB<T>|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` 回调函数 传回拷贝后的值; ## 8.2. 预设presetTypeCopierMap 如果经常需要给 `deepCopy()` 传递包含相同 类型和拷贝者 的 `typeCopyers` 参数,则可以将那些经常用到的 拷贝者 设置到 `deepCopy()` 的预设 `deepCopy.presetTypeCopierMap` 中; `deepCopy` 函数对象有个属性 `presetTypeCopierMap` 是用来设置常用的拷贝者的,它的类型是 `Map<Types, Reviver>`,所以上面的例子也可以如下设置预设: ``` // 当需要拷贝 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` 类型的拷贝者了; ## 8.3. 拷贝者的优先级 当需要拷贝某个类型的实例时,`deepCopy()` 会通过以下几种方法获取该实例的副本: - 首先使用 `typeCopyers` 参数中的拷贝者; - 如果没有找到拷贝者,则再使用预设 `presetTypeCopierMap` 中的拷贝者; - 如果没有找到拷贝者,则会将实例的 `getCopy` 方法作为拷贝者; - 如果实例没有 `getCopy` 方法,则会使用 `typeCopyers` 参数中 `"default"` 对应的 拷贝者; - 如果没有找到拷贝者,则会使用预设 `presetTypeCopierMap``"default"` 对应的 拷贝者; - 如果没有找到拷贝者,则会使用内置的默认拷贝逻辑; 其中,`"default"` 对应的拷贝者称为 默认的拷贝者; ## 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; }; ``` # 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); ```