目录
- 1. 背景
- 2. 简介
- 3. 安装方式
- 3.1. 方式1:通过 npm 安装
- 3.2. 方式2:直接下载原代码
- 3.3. 方式3:通过
<script>
标签引入
- 4. 教程
- 4.1. API简介
- 4.2. 基本使用
- 4.3. 拷贝函数
- 4.4. 指定拷贝深度
- 4.5. 循环引用
- 4.6. 保持类型信息
- 4.7. 拷贝不可枚举的属性
- 4.8. 自定义拷贝规则
- 4.8.1. typeCopyers
- 4.8.1.1. Types
- 4.8.1.2. 拷贝者Copyer
- 4.8.2. 预设presetTypeCopierMap
- 4.8.3. 拷贝者的优先级
- 4.8.4. getCopy
- 4.8.1. typeCopyers
- 4.9. 创建带预设拷贝规则的拷贝函数
内容
1. 背景
开发中,经常需要对一个对象进行深拷贝操作,目前经常用到的深拷贝的方式有以下几种:
- 将对象序列化成 JSON 字符串后,再反序化成对象
let copy = JSON.parse(JSON.stringify(value))
- 第三方库提供的深拷贝工具,如 Lodash 的
_.cloneDeep(value)
和_.cloneDeepWith(value, customizer)
但这些方法有以下缺点:
- 不支持对象成员循环引用,比如下面这种:
- 拷贝后会丢失类型信息,变成了普通的对象
- 拷贝后会丢失成员引用关系信息
- 不能根据类型自定义拷贝规则
- 只能拷贝可枚举的属性
- 不能拷贝函数
- 不能指定拷贝深度
为了解决这些问题,deep-copy 就出现了👏
2. 简介
deep-copy 是一个深拷贝工具,可对任意数据进行深度拷贝,包括 函数 function、正则 RegExp、Map、Set、Date、Array、URL 等等;支持含循环引用关系的对象的拷贝,并且不会丢失成员的引用关系信息 和 类型信息,支持扩展,可根据数据类型定制拷贝逻辑,也可指定拷贝深度;所以,通过它可实现对任意类型的数据进行任意想要的拷贝;
具有以下特性:
- 支持对象成员循环引用
- 拷贝后不会丢失类型信息 和 成员引用关系信息
- 可指定拷贝深度
- 即能拷贝可枚举的成员,也可拷贝不可枚举的成员
- 可拷贝函数
- 可根据类型自定义拷贝规则
- 支持预设拷贝规则
- 支持创建多个不同预设拷贝规则的拷贝函数
详情请看:
- 主页:https://github.com/GuoBinyong/deep-copy
- GitHub仓库
- 码云仓库
如果您在使用的过程中遇到了问题,或者有好的建议和想法,您都可以通过以下方式联系我,期待与您的交流:
- 给该仓库提交 issues
- 给我 Pull requests
- 邮箱:guobinyong@qq.com
- QQ:guobinyong@qq.com
- 微信:keyanzhe
3. 安装方式
目前,安装方式有以下几种:
3.1. 方式1:通过 npm 安装
npm install @gby/deep-copy
3.2. 方式2:直接下载原代码
您可直接从项目的 发行地址 下载 源码 或 构建后包文件;
您可以直接把 源码 或 构建后 的包拷贝到您的项目中去;然后使用如下代码在您的项目中引入 deepCopy
:
import { deepCopy } from "path/to/package/deep-copy";
或者
import deepCopy from "path/to/package/deep-copy";
3.3. 方式3:通过<script>
标签引入
您可直接从项目的 发行地址 中下载以 .iife.js
作为缀的文件,然后使用如下代码引用 和 使用 deep-copy:
-
引用 deep-copy
<script src="path/to/package/deep-copy.iife.js"></script>
-
使用全局的
deepCopy
<script> // 使用全局的 deepCopyconst copy = deepCopy.deepCopy(value); </script>
4. 教程
4.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;
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个参数并设置选项 copyFun
为 true
,如下所示:
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
;表示拷贝的最大深度;当值为 undefined
或 null
时,会使用默认值,表示无限深度;被拷贝的值(本例中是 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 = "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 在拷贝实例时,会用实例所属的类创建一个新的实例,创建时并不会给构造函数传递任何参数, 然后将原来实例本身的成员的副本重设 新实例为新实例的成员;这对于那些需要给构造函数传递参数的类 可能会存在问题;如果某类创建实例时依赖构造函数的参数,则您可以针对些类定制拷贝规则;
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 = "guobinyong@qq.com";
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>
类型的,是用来描述 类型 和 拷贝者 Copier(提供拷贝实例回调函数)的对应关系的,上例中 typeCopyers
使用的是 对象形式,它共以下几种描述方式:
{[TypeName:ExactTypeName]:Copyer}
:对象形式,属性名字 是 类型的名字,属性值是类型的 拷贝者(Copyer);[Types, Copyer][]
:数组形式,数组中的元素是[Types, Copyer]
类型的元组,元组的第一个元素是Types
,第二个元素是 拷贝者;Map<Types, Copyer>
:Map形式,Map中的键是Types
类型,值是 拷贝者;
4.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]
;
4.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
回调函数 传回拷贝后的值;
4.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
类型的拷贝者了;
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);