什么是深拷贝
- 简单理解
b是a的一份拷贝,b中没有对a中对象的引用
- 另一种理解
b是a的一份拷贝,对b的修改不会影响到a
解决方案
1.JSON序列化反序列化
const cloneTarget = JSON.parse(JSON.stringify(target))
缺点:
- 不支持函数 (函数会被忽略)
- 不支持undefined (undefined会被忽略)
- 不支持循环引用
- 不支持Date (会转成ISO8601字符串)
- 不支持正则 (会转成空对象)
- 不支持Symbol (忽略)
2.递归克隆
思路:
- 递归
看节点的类型,如果是基本类型则直接拷贝,如果是Object则分开讨论
- object分为 普通对象 - for in 数组 array - Array 初始化 函数 function - 怎么拷贝?闭包? 日期 Date - 怎么拷贝 正则表达式 RegExp - 怎么拷贝
实现
接下来我们使用测试驱动开发来实现一个深拷贝
环境准备
- 创建目录
mkdir deepClone
cd deepClone
yarn init -y
- 引入chai、sinon和typescript
yarn add chai mocha sinon sinon-chai
yarn add @types/chai @types/mocha @types/sinon @types/sinon-chai ts-node typescript
- 安装完后,修改package.json
"scripts": { "test": "mocha -r ts-node/register test/**/*.ts"},
开始测试驱动开发
创建两个文件:
- src/index.ts 源码实现
- test/index.ts 测试用例
接下来开始测试驱动开发,我们先来写一个简单的测试用例
- 验证deepClone是一个函数
test/index.ts
import sinonChai from "sinon-chai"
import chai from 'chai'
import {describe} from 'mocha'
import deepClone from "../src";
const assert = chai.assert;
chai.use(sinonChai);
describe('deepClone', () => {
it('是一个函数', () => {
assert.isFunction(deepClone)
})
})
执行命令yarn test
,这时测试环境就会报错,因为我们还没开始写代码。
接下来我们开始写代码来通过这个测试用例
src/index.ts
function deepClone(){}
export default deepClone
再执行yarn test
,控制台就会提示测试用例通过。
这样,我们一个简单的测试驱动用例就完成了。
接下来我们由浅入深一步步实现深拷贝的代码。
- 能复制基本类型
在test/index.ts增加一个新的测试用例
it("能够复制基本类型", () => {
const n = 1;
const n2 = deepClone(n)
assert(n === n2)
const s = '112'
const s2 = deepClone(s)
assert(s === s2)
const b = true
const b2 = deepClone(b)
assert(b === b2)
const u = undefined
const u2 = deepClone(u)
assert(u === u2)
const empty = null
const empty2 = deepClone(empty)
assert(empty === empty2)
const sym = Symbol()
const sym2 = deepClone(sym)
assert(sym === sym2)
});
运行yarn test
,会提示用例未通过
实现代码
src/index.ts
function deepClone(target){
return target // 普通类型只要直接返回即可
}
export default deepClone
再运行yarn test
,会提示用例已通过
- 能复制普通对象
增加一个新的测试用例,用来测试复制普通对象 test/index.ts
describe('对象', () => {
it('能复制普通对象', () => {
const a = {name: 'lzb', address: {zipCode: '000000'}}
const a2 = deepClone(a)
assert(a !== a2) // 对象是不相等的
assert(a.name === a2.name) // 普通属性相等
assert(a.address !== a2.address) // 对象属性不相等
assert(a.address.zipCode === a2.address.zipCode)
})
代码实现 src/index.ts
type Dictionary = {[key:string]:any}
function deepClone(target:any){
// 判断是否对象类型
if(typeof target==='object'){
let cloneTarget:Dictionary = {}
for(let key in target){
// 遍历每个属性并返回复制后的值
cloneTarget[key] = deepClone(target[key])
}
return cloneTarget
}
return target // 普通类型只要直接返回即可
}
export default deepClone
- 能复制数组对象
test/index.ts
it('能复制数组对象', () => {
const a = [[11, 12], [21, 22], [31, 32]]
const a2 = deepClone(a)
assert(a2 !== a)
assert(a2[0] !== a[0])
assert(a2[1] !== a[1])
assert(a2[2] !== a[2])
assert.deepEqual(a, a2)})
src/index.ts
...
if(target instanceof Object){
let cloneTarget:Dictionary
if(target instanceof Array){
cloneTarget = [] //如果是数组,则初始化为空数组 }else{
cloneTarget = {} //否则初始化为空对象
}
for(let key in target){
// 遍历每个属性并返回复制后的值
cloneTarget[key] = deepClone(target[key])
}
return cloneTarget
}
...
- 能复制函数
增加测试用例 test/index.ts
...
it('能复制函数', ()=>{
const a = function (a:number,b:number){return a+b} const a2 = deepClone(a)
assert(a!==a2)
assert(a(1,2)===a2(1,2))
})
...
运行yarn test
src/index.ts
if(target instanceof Array){
cloneTarget = [] //如果是数组,则初始化为空数组
}else if(target instanceof Function){
cloneTarget = function (this:any){
return target.apply(this, arguments)
}
}else{
cloneTarget = {} //否则初始化为空对象
}
...
再运行yarn test
,控制台显示通过
- 能复制环
增加测试用例
type Dictionary = { [key: string]: any }
...
it('能复制环', ()=>{
const a:Dictionary = {x:{y:111}}
a.self = a
const a2 = deepClone(a)
assert(a !== a2)
assert(a.x!==a2.x)
assert(a.x.y===a2.x.y)})
写代码让测试用例通过
type Dictionary = {[key:string]:any}
// 用map来缓存访问过的对象
const map = new Map()
function deepClone(target:any){
// 判断是否对象类型
if(target instanceof Object){
// 判断是否缓存过
if(map.has(target)){
// 有则直接返回
return map.get(target)
}else{
let cloneTarget:Dictionary
if(target instanceof Array){
cloneTarget = [] //如果是数组,则初始化为空数组
}else if(target instanceof Function){
cloneTarget = function (this:any){
return target.apply(this, arguments)
}
}else{
cloneTarget = {} //否则初始化为空对象
}
// 需要在递归之前缓存
map.set(target,cloneTarget)
for(let key in target){
// 遍历每个属性并返回复制后的值
cloneTarget[key] = deepClone(target[key])
}
return cloneTarget
}
}
return target // 普通类型只要直接返回即可
}
export default deepClone
- 复制正则表达式
test/index.ts
it('复制正则表达式', () => {
const a = new RegExp('hi\d+', 'ig')
const a2 = deepClone(a)
assert(a !== a2)
assert(a.source === a2.source)
assert(a.flags === a2.flags)
})
src/index.ts
...
}else if(target instanceof RegExp){
// 正则表达式对象会有两个属性,source和flags,分别对应构造函数的两个参数
cloneTarget = new RegExp(target.source, target.flags)
}
...
- 复制日期
test/index.ts
it('复制日期', ()=>{
const a = new Date()
const a2:Date = deepClone(a)
assert(a!==a2)
assert(a.getTime()===a2.getTime())
})
src/index.ts
...
else if(target instanceof Date){
cloneTarget = new Date(target)
}
...
- 自动跳过原型属性
test/index.ts
it('跳过原型属性', ()=>{
const a = Object.create({name:'a'})
a.x = {yyy:1}
const a2 = deepClone(a)
assert(a!==a2)
assert.isFalse('name' in a2)
assert(a.x!==a2.x)
assert(a.x.y===a2.x.y)
})
src/index.ts
for(let key in target){
// 过滤原型属性
if(target.hasOwnProperty(key)) {
// 遍历每个属性并返回复制后的值
cloneTarget[key] = deepClone(target[key])
}
}
- 复制很复杂的对象
test/index.ts
it("很复杂的对象", () => {
const a = {
n: NaN,
n2: Infinity,
s: "",
bool: false,
null: null,
u: undefined,
sym: Symbol(),
o: {
n: NaN,
n2: Infinity,
s: "",
bool: false,
null: null,
u: undefined,
sym: Symbol()
},
array: [
{
n: NaN,
n2: Infinity,
s: "",
bool: false,
null: null,
u: undefined,
sym: Symbol()
}
]
};
const a2 = deepClone(a);
assert(a !== a2);
assert.isNaN(a2.n);
assert(a.n2 === a2.n2);
assert(a.s === a2.s);
assert(a.bool === a2.bool);
assert(a.null === a2.null);
assert(a.u === a2.u);
assert(a.sym === a2.sym);
assert(a.o !== a2.o);
assert.isNaN(a2.o.n);
assert(a.o.n2 === a2.o.n2);
assert(a.o.s === a2.o.s);
assert(a.o.bool === a2.o.bool);
assert(a.o.null === a2.o.null);
assert(a.o.u === a2.o.u);
assert(a.o.sym === a2.o.sym);
assert(a.array !== a2.array);
assert(a.array[0] !== a2.array[0]);
assert.isNaN(a2.array[0].n);
assert(a.array[0].n2 === a2.array[0].n2);
assert(a.array[0].s === a2.array[0].s);
assert(a.array[0].bool === a2.array[0].bool);
assert(a.array[0].null === a2.array[0].null);
assert(a.array[0].u === a2.array[0].u);
assert(a.array[0].sym === a2.array[0].sym);
});
- 不会爆栈
it("不会爆栈", () => {
const a = { child: null };
let b:Dictionary = a;
for (let i = 0; i < 10000; i++) {
b.child = {
child: null
};
b = b.child;
}
const a2 = deepClone(a);
assert(a !== a2);
assert(a.child !== a2.child);
});
src/index.ts
type Dictionary = { [key: string]: any }
// 用map来缓存访问过的对象
const map = new Map()
function deepClone(target: any) {
const stack: any = []
if (target instanceof Object) {
// 把根节点入栈
const root = {parent: null, key: null, value: target}
stack.push(root)
let current = stack.pop()
let cloneTarget
while (current) {
const nodeValue = current.value
// 根据对象类型不同创建不同的对象
const temp: Dictionary = createObject(nodeValue)
// 缓存对象
map.set(nodeValue, temp)
// 遍历属性
for (let k in nodeValue) {
if (!nodeValue.hasOwnProperty(k)) continue
// 如果属性值已经缓存,则把缓存的值返回
if (map.has(nodeValue[k])) {
temp[k] = map.get(nodeValue[k])
} else if (nodeValue[k] instanceof Object) {
// 如果属性值是一个对象,则先入栈,后续再处理
stack.push({parent: temp, key: k, value: nodeValue[k]})
} else {
// 属性值是普通类型,则直接赋值
temp[k] = nodeValue[k]
}
}
// 父节点不存在则把临时变量赋值给cloneTarget,存在则把临时变量赋值给父节点的key
current.parent ? (current.parent[current.key] = temp) : (cloneTarget = temp)
// 处理下一个节点
current = stack.pop()
}
return cloneTarget
}
return target
}
const createObject = (target: Object) => {
let obj: Dictionary
if (target instanceof Array) {
obj = [] //如果是数组,则初始化为空数组
} else if (target instanceof Function) {
obj = function (this: any) {
return target.apply(this, arguments)
}
} else if (target instanceof RegExp) {
// 正则表达式对象会有两个属性,source和flags,分别对应构造函数的两个参数
obj = new RegExp(target.source, target.flags)
} else if (target instanceof Date) {
obj = new Date(target)
} else {
obj = {} //否则初始化为空对象
}
return obj
}
export default deepClone
思路:把递归改成用栈和循环的方式实现, 1.首先把根节点入栈, 2.然后遍历栈,如果栈有数据,则取出栈顶。 3.先创建一个临时的对象,然后遍历栈顶的对象属性,如果属性值是对象,则根据属性和属性值创建一个新的节点,并入栈。如果属性值是普通类型的则直接赋值。 4.继续处理栈的数据,直到栈为空。
源码地址
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!