方案1.序列化反序列化
var a = {name: 'lifa',age:18}
var b = JSON.parse(JSON.stringify(a))
上面的代码b就是a的深拷贝当我们修改b里面的值的时候,a的值不会跟着变化。
1.1.缺点
1). 不支持function
如果被深拷贝的对象里面有函数的话,我们深拷贝后的对象会直接将函数忽略
2). 不支持undefined
如果被深拷贝的对象里面有undefined,我们使用JSON对其深拷贝后,深拷贝后的对象也会将undefined忽略
3). 不支持循环引用(不能把自己赋值给自己内部的属性)
报错,JSON只支持竖状的结构不支持环状结构
4). 会把Date类型变成字符串
Date类型经过JSON深拷贝后变成了ISO8601格式的字符串
5). 不支持正则表达式
正则表达式经过JSON后会变成空对象
6). 不支持Symbol
方案2. 递归克隆
2.1 思路
- 递归
看节点的类型(7种)
如果是基本类型直接拷贝
如果是object就分情况讨论(普通object、数组array、函数function、日期Date)
2.2 步骤
创建目录
引入chai和sinon
开始驱动测试开发
测试失败=> 改代码 => 测试成功 =>加测试 => 测试失败
- src/index.js
function deepCLone() {
}
module.exports = deepCLone
- test/index.js
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const deepCLone = require('../src/index')
chai.use(sinonChai)
const assert = chai.assert
const deepClone = require('../src/index')
describe('deepClone', () => {
it('是一个函数', () => {
assert.isFunction(deepCLone)
})
})
- package.json
{
"name": "deep-clone",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "mocha test/**/*.js"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.2.0",
"sinon": "^7.4.1",
"sinon-chai": "^3.3.0"
}
}
运行yarn test
2.2.1. 对基本类型的拷贝
对于基本类型直接传什么就返回什么
function deepClone(source) {
return source
}
单元测试
it('可以复制基本类型', () => {
const n = 123
const n2 = deepClone(n)
assert(n ===n2)
const b = '123456'
const b2 = deepClone(b)
assert(b === b2)
const c = undefined
const c2 = deepClone(c)
assert(c === c2)
const d = null
const d2 = deepClone(d)
assert(d === d2)
const e = true
const e2 = deepClone(e)
assert(e === e2)
const f = Symbol()
const f2 = deepClone(f)
assert(f === f2)
})
2.2.2. 对于普通对象的深拷贝
先判断传入的参数类型是不是对象,然后创建一个新的对象,遍历入参对象的每一项key,将每一项的值都进行深拷贝然后把当前项赋值给新的对象
if (source instanceof Object) {
const deepObj = new Object()
for (let key in source) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
单元测试
it('可以复制普通对象', () => {
const a = { name: '立发', child: { name: '小立发'}}
const a2 = deepClone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.child !== a2.child)
assert(a.child.name === a2.child.name)
})
2.2.3. 对于数组对象的深拷贝
if (source instanceof Object) {
if (source instanceof Array) {
const deepObj = new Array()
for (let key in source) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
}
单元测试
it('可以复制数组对象', () => {
const a = [[1, 2], [4, 5], [6, 7]]
const a2 = deepClone(a)
assert(a !== a2)
assert(a[0] !== a2[0])
assert(a[1] !== a2[1])
assert(a[2] !== a2[2])
assert.deepEqual(a, a2)
})
2.2.4. 对于函数对象的深拷贝
判断一个函数是不是另一个函数的深拷贝,首先函数也是一个对象所以它应该满足对象深拷贝的条件
1).深拷贝后的函数应该不等于源函数
2).深拷贝后的函数应该和源函数的基本类型的属性相等
3).深拷贝后的函数应该和源函数的引用类型的属性不相等
其次针对于函数对象本身
4).深拷贝后的函数的执行结果应该和源函数的执行结果相等
针对上面四点的单元测试如下:
it('可以复制函数', () => {
const a = function(x, y) {
return x + y
}
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a)
assert(a !== a2) //上面的1)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz) //2)
assert(a.xxx.yyy !== a2.xxx.yyy) //3)
assert(a.xxx !== a2.xxx) //3)
assert(a(1,2) === a2(1,2)) //4)
})
- index.js
else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
for (let key in source) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
实际上就是声明一个新的函数,然后在这个新的函数里把你传入的函数调用一遍(把当前新的函数的this传给传入的函数,然后把参数放进去)然后再return它
2.2.5. 对于环的深拷贝
我们上面的代码如果出现环引用(某个属性的值等于本身的引用地址)的话会出现死循环现象,所以我们需要先把我们每次的值存下来(以此来确定某个属性的值有没有被深拷贝过),如果下次调用发现某个属性的值已经深拷贝过了就直接返回第一次深拷贝过的值,否则就继续递归。
上面图中黑色的环路是我们传入的需要深拷贝的源对象,红色的环路是我们深拷贝后的。我们看到了一个1就把1直接存储下来(不经过深拷贝处理,如果是引用类型直接把引用地址存储下来),然后对1进行深拷贝生成一个新的1',然后看到2把2直接存储下来,然后对2进行深拷贝生成一个新的2',然后走到我们的3后面我们应该接的实际上是我们第一次拷贝好的1',所以就需要我们每次存储的时候把我们一开始的原始值和深拷贝后的值都存储下来,通过原始值来得到我们第一次深拷贝后的值
let cache = []
function deepClone(source) {
if (source instanceof Object) {
let cacheDist = findCache(source)
if (cacheDist) {
return cacheDist
} else {
let deepObj
if (source instanceof Array) {
deepObj = new Array()
} else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
} else {
deepObj = new Object()
}
cache.push([source, deepObj])
for (let key in source) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
}
return source
}
function findCache(source) {
for (let i = 0; i < cache.length; i++) {
if (cache[i][0] === source) {
return cache[i][1]
}
}
return undefined
}
单元测试
it('环也能复制', () => {
const a = { name: '立发'}
a.self = a
const a2 = deepClone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.self !== a2.self)
})
2.2.6. 正则表达式的深拷贝
我们对正则表达式的声明主要有两部分,一部分是它的文本,一部分是它的标志
const a = new RegExp('hi\\d+', 'gi')
上面的hi\d+就是它的文本,gi就是标志,所以我们要对它进行深拷贝的话只需要拿到它的文本和标志然后放到一个新的RegExp对象里即可,那么我们如何拿到这两部分的值那,通过a.source就可以拿到它的文本值,a.flags就可以拿到标志的值
if (source instanceof Object) {
else if (source instanceof RegExp) {
deepObj = new RegExp(source.source, source.flags)
}
}
单元测试
it('可以复制正则表达式', () => {
const a = new RegExp("hi\\d+", 'gi')
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
assert(a.source === a2.source)
assert(a.flags === a2.flags)
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.2.6. Date类型的深拷贝
我们只需要判断如果是Date类型,就直接把当前的Date放到一个新的Date对象里即可,我们想要判断两个Date的值是否相等只需要通过getTime()方法就可以
if (source instanceof Object) {
else if (source instanceof Date) {
deepObj = new Date(source)
}
}
单元测试
it('可以复制日期', () => {
const a = new Date()
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
assert(a.getTime() === a2.getTime())
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.2.7. 自动跳过原型属性
如果我们通过Object.create给对象添加一个属性的时候,这个属性会被添加到它的proto里也就是原型上
上图中我们通过Object.create创建了一个a对象并添加了name属性,但是a本身没有name属性,而是在proto里,对于这种在原型上的属性我们不应该去拷贝(原因:如果原型上的每一层都拷贝的话会造成内存爆掉),那么我们怎么实现只拷贝本身的属性哪?
办法:在for in里加上限制条件
for (let key in source) {
// 如果key是source对象自身的属性才去进行拷贝
if (source.hasOwnProperty(key)) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = deepClone(source[key])
}
}
单元测试
it('自行跳过原型属性', () => {
const a = Object.create( { name: 'lifa' })
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
//a2上没有name属性
assert.isFalse('name' in a2)
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.3.问题
上面的代码的问题:cache每次复制完一个对象的时候都没有被清空,所以下一次深拷贝的时候就会互相影响
解决方法:使用面向对象,每次实例化的时候生成一个新的cache
完整代码
class DeepCloner {
constructor () {
this.cache = []
}
clone(source) {
if (source instanceof Object) {
let cacheDist = this.findCache(source)
if (cacheDist) {
return cacheDist
} else {
let deepObj
if (source instanceof Array) {
deepObj = new Array()
} else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
deepObj = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
deepObj = new Date(source)
} else {
deepObj = new Object()
}
this.cache.push([source, deepObj])
for (let key in source) {
if (source.hasOwnProperty(key)) {
// 对对象里的每一项进行深拷贝并把这一项赋值给新的对象
deepObj[key] = this.clone(source[key])
}
}
return deepObj
}
}
return source
}
findCache(source) {
for (let i = 0; i < this.cache.length; i++) {
if (this.cache[i][0] === source) {
return this.cache[i][1]
}
}
return undefined
}
}
module.exports = DeepCloner