关于深拷贝
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
深拷贝是前端面试的必考题之一,那么今天就来学习手写一个深拷贝函数吧!
什么是深拷贝?
首先先来看两个小问题
问题一:
let a = 1
b = a
b = 2
// a = ?
答案:a = 1
问题二:
let a = { name: 'test' }
b = a
b.age = 18
// a.age = ?
答案:a.age = 18
为什么会这样呢?
出现这样的原因,是因为 js 的 Object 类型的赋值是一个引用,也就是说 a = {name: 'test'} 这个 a 其实指向的是 {name: 'test'} 这个对象的地址。而 b = a 这个操作,就是把 a 指向的地址赋值给 b,那么 a、b 就会指向同一个地址,也就都指向了 {name: 'test'} 这个对象。
而这样的一个复制拷贝的过程,也就是浅拷贝
那么如果我想要修改 b 对象,但是不修改 a 对象的值,应该怎么做呢? 这就需要重新开辟一个内存空间,用来存放 b 指向的对象,这一过程就是深拷贝。
如何实现深拷贝?
简易方法:
最简单,也是工作中使用最多的,能解决大部分问题的方法,JSON 序列化
const a = {name: 'test'}
const b = JSON.parse(JSON.stringify(a))
b.age = 18
console.log(a.age)// undefined
但是这个方法也会有一个问题,如果 a 对象中的 value 存在函数、undefined、Date、RegExp,那么 JSON 序列化的结果就会丢失、转化成另一个格式的内容等结果,原因是 JSON 不支持这些数据类型。也就从这里,引出了手写一个深拷贝的需求。 示例:
进阶方法
- 首先先来写一个能够拷贝普通对象和基本类型的实现
class DeepClone {
clone(source) {
if (source instanceof Object) {
let dist = {}
for(let key in source) {
dist[key] = this.clone(source[key])
}
return dist
} else {
return source
}
}
}
这里我们编写一个简单的测试用例,确保这个方法是正常的
describe('DeepClone', () => {
it('他是一个类', () => {
assert.isFunction(DeepClone)
})
it('能够复制基本类型', () => {
const number = 123
const deepClone = new DeepClone()
const number2 = deepClone.clone(number)
assert(number === number2)
const string = '123456'
const string2 = deepClone.clone(string)
assert(string === string2)
const boolean1 = true
const boolean2 = deepClone.clone(boolean1)
assert(boolean1 === boolean2)
const u = undefined
const u2 = deepClone.clone(u)
assert(u === u2)
const empty = null
const empty2 = deepClone.clone(empty)
assert(empty === empty2)
const symbol = Symbol()
const symbol2 = deepClone.clone(symbol)
assert(symbol === symbol2)
})
describe('对象', () => {
it('能够复制普通对象', () => {
const a = {name: 'wwwbh', child: {name: 'xwwwbh', age: 1}}
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.child !== a2.child)
assert(a.child.name === a2.child.name)
assert(a.child.age === a2.child.age)
})
})
})
此时运行测试可以看到,我们函数复制普通类型和简单对象都是没有问题的
- 接下来加上 Array、Function、RegExp、Date 的类型
class DeepClone {
clone(source) {
if (source instanceof Object) {
let dist
if (source instanceof Array) {
dist = []
} else if (source instanceof Function) {
dist = function() {
return source.call(this, ...arguments)
}
} else if (source instanceof Date) {
dist = new Date(source)
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else {
dist = {}
}
for(let key in source) {
dist[key] = this.clone(source[key])
}
return dist
} else {
return source
}
}
}
完善测试用例:
describe('DeepClone', () => {
// ...
describe('对象', () => {
it('能够复制普通对象', () => {
const a = {name: 'wwwbh', child: {name: 'xwwwbh', age: 1}}
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.child !== a2.child)
assert(a.child.name === a2.child.name)
assert(a.child.age === a2.child.age)
})
it('能够复制数组对象', () => {
const a = [[11, 12], [21, 22], [31, 32]]
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a[0] !== a2[0])
assert(a[1] !== a2[1])
assert(a[2] !== a2[2])
assert.deepEqual(a, a2)
})
it('能够复制函数对象', () => {
const a = function (x, y) {
return x + y
}
a.xxx = {yyy: {zzz: 1}}
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a.xxx !== a2.xxx)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a(1, 2) === a2(1, 2))
})
it('可以复制正则', () => {
const a = new RegExp('hi\d+', 'gi')
a.xxx = {yyy: {zzz: 1}}
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a.source === a2.source)
assert(a.flags === a2.flags)
assert(a.xxx !== a2.xxx)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
})
it('可以复制日期', () => {
const a = new Date()
a.xxx = {yyy: {zzz: 1}}
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a.getTime() === a2.getTime())
assert(a.xxx !== a2.xxx)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
})
})
})
此时运行我们的测试用例
再次进阶
目前我们的 DeepClone 还不支持环引用,也就是这样的数据类型:
如果把这样的数据类型放到我们的 DeepClone 中,无疑会触发递归死循环,直接爆栈。
为了解决这个问题,我们需要一个 cache 来存储每次传递进来的 source,同时拿这个source来与 cache 做比较,是否存在引用相同地址的对象,如果没有,就放入 cache 中,如果存在,就直接返回。由此来解决环引用的问题。
class DeepClone {
cache = []
clone(source) {
if (source instanceof Object) {
let dist
const cacheDist = this.findCache(source)
if (cacheDist){
return cacheDist
} else {
if (source instanceof Array) {
dist = []
} else if (source instanceof Function) {
dist = function() {
return source.call(this, ...arguments)
}
} else if (source instanceof Date) {
dist = new Date(source)
} else if (source instanceof RegExp) {
dist = new RegExp(source.source, source.flags)
} else {
dist = {}
}
this.cache.push([source, dist])
for(let key in source) {
dist[key] = this.clone(source[key])
}
return dist
}
} else {
return source
}
}
findCache(source){
for(let i = 0; i < this.cache.length; i ++) {
if (this.cache[i][0] === source) {
return this.cache[i][1]
}
}
}
}
完善测试用例:
describe('DeepClone', () => {
// ...
describe('对象', () => {
//...
it('能够复制环引用的对象', () => {
const a = {name: 'wwwbh'}
a.self = a
const deepClone = new DeepClone()
const a2 = deepClone.clone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.self !== a2.self)
})
})
})
此时再次运行测试
那么环引用对象的深拷贝,也被我们解决了
最后的进化
最后一点,一般来说,深拷贝不需要复制原型链上的方法对象,那么就使用 hasOwnProperty 方法来做个判断。
最终代码
class DeepClone {
cache = []
clone(source) {
if (source instanceof Object) {
let cachedDist = this.findCache(source)
if (cachedDist) {
return cachedDist
} else {
let dist
if (source instanceof Object) {
if (source instanceof Array) {
dist = []
} else if (source instanceof Function) {
dist = function () {
return source.call(this, ...arguments)
}
} else if (source instanceof RegExp){
dist = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
dist = new Date(source)
}else {
dist = {}
}
this.cache.push([source, dist])
for (let key in source) {
if(source.hasOwnProperty(key)){
// 只复制不在原型上的属性
dist[key] = this.clone(source[key])
}
}
return dist
}
}
}
return source
}
findCache(source) {
for (let i = 0; i < this.cache.length; i++) {
if (this.cache[i][0] === source) {
return this.cache[i][1]
}
}
}
}