深拷贝
说起深拷贝,常见的一个深拷贝方法就是JSON.parse(JSON.stringify(object))。但这个方法有一些缺陷,导致一些特殊场景下,这个方法无法满足需求。例如,拷贝函数成员、循环引用。
这个问题的本质其实是JSON字符串不支持函数成员与循环引用。
const obj = { fn() {} }
console.log(JSON.stringify(obj)) // 这里的结果是一个空对象{}
const obj = {}
obj.self = obj;
JSON.stringify(obj) // TypeError: Converting circular structure to JSON
下面,我们渐进式的写一个较完善的深拷贝。
1. 从浅拷贝开始
function Test() {
console.assert(null === clone(null), 'null')
console.assert(undefined === clone(undefined))
console.assert(1 === clone(1), 'number')
console.assert(true === clone(true), 'boolean')
console.assert('1' === clone('1'), 'string')
const s = Symbol()
console.assert(s === clone(s))
const a = {
foo: 'bar'
}
const b = {
name: 'hello'
}
const obj = {
a,
b,
}
const objClone = clone(obj)
console.assert(obj !== objClone, '对象拷贝后引用不相同')
console.assert(obj.a === objClone.a, '浅拷贝后内部对象引用相同')
console.assert(obj.b === objClone.b, '浅拷贝后内部对象引用相同')
}
Test()
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
const newObject = {}
for(const key in target) {
newObject[key] = target[key]
}
return newObject;
}
思路很简单,检查一下要克隆的值是不是一个对象,如果不是,直接返回;如果是一个对象,则通过for-in遍历属性,然后浅拷贝这些属性。
上面代码唯一要注意的一点就是,因为typeof null的值是'object',所以我们需要显示的处理target === null的情况;而对于undefined来说,typeof undefined的值是'undefined',就不需要特殊处理了。
当然,这里更简单的写法是if(!target || typeof target !== 'object'),上面是为了记录一下typeof null的特殊之处。
上面的代码有不少问题,我们一点一点解决。
1.1 问题1:到底拷贝哪些属性?使用for-in真的合适吗?
这个问题没有一个标准的答案,这个要看具体的需求。就拿for-in来说,它会遍历自身以及原型链上所有的可枚举的string类型的key。
如果这正是你需要的,那它就是合适的。但在这里,我们想要构造两个一摸一样的对象,甚至原型链也一摸一样,就不能把原型上的属性变成新对象的自有属性。
这里,我们不再使用for-in,而是使用Reflect.ownKeys();这样,就可以获取对象的所有自由属性,包括不可枚举的属性和key为symbol类型的属性。
关于属性的遍历,可以看我的另一篇文章Javascript属性遍历与可迭代对象 - 掘金 (juejin.cn)
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
const newObject = {}
// 不使用for-in
for(const key of Reflect.ownKeys(target)) {
newObject[key] = target[key]
}
return newObject;
}
还有一个刚才提到的问题,现在我们不遍历原型链,那怎么浅拷贝原型链上的属性?换个思路嘛,直接拷贝原型链。
1.2 浅拷贝原型链
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
// 浅拷贝原型链
const newObject = Object.create(Object.getPrototypeOf(target))
for(const key of Reflect.ownKeys(target)) {
newObject[key] = target[key]
}
return newObject;
}
最后我们改一下测试用例,已经完美通过了:
const proto = {
prop: 'value'
}
const a = {
foo: 'bar'
}
const b = {
name: 'hello'
}
const obj = Object.create(proto, {
a,
b,
})
const objClone = clone(obj)
console.assert(obj !== objClone, '对象拷贝后引用不相同')
console.assert(obj.a === objClone.a, '浅拷贝后内部对象引用相同')
console.assert(obj.b === objClone.b, '浅拷贝后内部对象引用相同')
console.assert(obj.prop === objClone.prop, '浅拷贝原型链')
console.assert(Object.getPrototypeOf(obj) === Object.getPrototypeOf(objClone), '浅拷贝原型链')
2. 深拷贝
2.1 简单深拷贝
现在改一下我们的测试用例:
const a = {
foo: 'bar'
}
const b = {
name: 'hello'
}
const obj = {
a,
b,
}
const objClone = clone(obj)
console.assert(obj !== objClone, '对象拷贝后引用不相同')
// 下面两句做了修改
console.assert(obj.a !== objClone.a, '浅拷贝后内部对象引用不相同')
console.assert(obj.b !== objClone.b, '浅拷贝后内部对象引用不相同')
要实现这样简单的深拷贝,其实只需要对上面的浅拷贝做一点点修改:
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
const newObject = Object.create(Object.getPrototypeOf(target))
for(const key of Reflect.ownKeys(target)) {
// newObject[key] = target[key]
newObject[key] = clone(target[key])
}
return newObject;
}
浅拷贝对象其实只拷贝了一层,但是,一旦递归的调用浅拷贝,就会一层一层的浅拷贝下去,最终就实现了深拷贝。
但是,如果尝试用这个函数拷贝一个数组,那问题就出来了:
const array = [1, 2, 3]
const arrayClone = clone(array)
console.log(array) // [ 1, 2, 3 ]
console.log(arrayClone) // Array { '0': 1, '1': 2, '2': 3, length: 3 }
数组变对象了!
2.1 深拷贝数组
所以,对于数组来说,我们并不能使用Object.create()函数创建普通对象,而是创建一个数组对象:
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
// 对数组进行特殊处理
if(Array.isArray(target)) {
return Array.from(target)
}
const newObject = Object.create(Object.getPrototypeOf(target))
for(const key of Reflect.ownKeys(target)) {
newObject[key] = clone(target[key])
}
return newObject;
}
好起来的,但是还有些问题,数组中的内容是浅拷贝的:
const array = [{a: 1 }, 2, 3]
const arrayClone = clone(array)
console.log(array) // [ { a: 1 }, 2, 3 ]
console.log(arrayClone) // [ { a: 1 }, 2, 3 ]
console.log(array[0] === arrayClone[0]) // true
再改一下,和之前浅拷贝改深拷贝的思路一样,递归就完事儿了~:
function clone(target) {
if(target === null || typeof target !== 'object') {
return target
}
if(Array.isArray(target)) {
// 戍主元素递归调用clone进行深拷贝
return Array.from(target).map(clone)
}
const newObject = Object.create(Object.getPrototypeOf(target))
for(const key of Reflect.ownKeys(target)) {
newObject[key] = clone(target[key])
}
return newObject;
}
2.2 循环引用问题
新的问题又来了,如果有循环引用怎么办:
const circular = { }
circular.self = circular
circularCloned = clone(circular) // RangeError: Maximum call stack size exceeded
我们上面的代码直接栈溢出了,原因是因为“我克隆我自己”,一直递归地克隆。
解决循环引用地思路是:在克隆过程中,遇到循环引用,就不要深拷贝了,而应该浅拷贝。
顺着这个思路,我们要解决以下两个问题:
- 怎么知道发生了循环引用?
- 既然要浅拷贝,我怎么拿到这个浅拷贝地引用?
首先,我们需要标记一个对象是不是正在克隆;在克隆的时候,检查一下这个对象有没有被标记,即是不是正在克隆,如果一个对象正在克隆,但它又被克隆了,这就说明发生了循环引用,需要进行浅拷贝了。
对于拷贝后的引用,我们可以用一个Map来存储,这个Map的key就是拷贝前的对象,Map的value就是拷贝后的对象;同时,这里也解决了上面的问题,如果一个对象在克隆的过程中,Map的key中已经存在该对象了,那就说明有循环引用。
由于在递归过程中要共享同一个Map,有两种做法:
- 通过参数传递
- 通过闭包
这里我们就使用闭包的方式吧:
function clone(target) {
const oldToNew = new Map()
// 把之前的clone改个名字
function _baseClone(target) {
if(target === null || typeof target !== 'object') {
return target
}
if(Array.isArray(target)) {
return Array.from(target).map(_baseClone)
}
const newRef = oldToNew.get(target)
// 如果当前对象已经在克隆了
if(newRef) { // 直接返回新对象的引用
return newRef;
}
const newObject = Object.create(Object.getPrototypeOf(target))
// 标记target正在克隆,并且保存新对象的引用
oldToNew.set(target, newObject)
for(const key of Reflect.ownKeys(target)) {
newObject[key] = _baseClone(target[key])
}
return newObject;
}
return _baseClone(target)
}
2.3 拷贝其它特殊对象 & Symbol.toStringTag
就像Array一样,还有一些其它的对象需要处理,例如Date、RegExp、Set、Map。
但是,它们和Array不一样的地方在于,可以使用Array.isArray()来判断一个对象是不是一个数组,其他对象并没有这样的一个方法。
instanceof这时候就派上用场了。但是,这里我们并不使用instanceof,没有其他原因,就是想多记录一点东西。
使用Object.prototype.toString.call()这样的方式,来获取一个描述类型的字符串。
const date = new Date()
const set = new Set()
const array = new Array()
console.log(Object.prototype.toString.call(date)) // [object Date]
console.log(Object.prototype.toString.call(set)) // [object Set]
console.log(Object.prototype.toString.call(array)) // [object Array]
但是为什么我们自定义的类型创建的对象得到的是[object Object]呢?关键是一个key为Symbol.toStringTag的属性;
我们可以在Set的原型上找到这个属性,它的值为"Set"。
class Person {
}
const p = new Person()
console.log(Object.prototype.toString.call(p)) // [object Object]
Person.prototype[Symbol.toStringTag] = 'Person'
console.log(Object.prototype.toString.call(p)) // [object Person]
注意:对于Array和Date,我没有在它们的prototype上找到这个属性。但是,它们得到的结果格式都是一致的。
提取类型名字:
function getObjectType(object) {
return Object.prototype.toString.call(object, 8, -1);
}
回到上面的代码,我们现在是要处理一些特殊对象:
function clone(target) {
const oldToNew = new Map()
function getObjectType(object) {
return Object.prototype.toString.call(object).slice(8, -1);
}
function _baseClone(target) {
if(target === null || typeof target !== 'object') {
return target
}
if(Array.isArray(target)) {
return Array.from(target).map(_baseClone)
}
// 一些特殊对象的处理
// 这里仅列举一部分
switch(getObjectType(target)) {
case 'Date': return new Date(target)
case 'RegExp': return new RegExp(target)
case 'Set': {
const newSet = new Set()
target.forEach(item => {
// 深拷贝内容元素
newSet.add(_baseClone(item))
})
return newSet
}
}
const newRef = oldToNew.get(target)
if(newRef) {
return newRef;
}
const newObject = Object.create(Object.getPrototypeOf(target))
oldToNew.set(target, newObject)
for(const key of Reflect.ownKeys(target)) {
newObject[key] = _baseClone(target[key])
}
return newObject;
}
return _baseClone(target)
}
上面的代码我们添加了对于Date、RegExp和Set的克隆处理。对于Set来说,它和Array一样,也需要深拷贝它的内容元素。
2.4 深拷贝原型链
如果想要深拷贝原型链,还是老思路,将浅拷贝原型链出的代码const newObject = Object.create(Object.getPrototypeOf(target))改成深拷贝:
function clone(target) {
const oldToNew = new Map()
function getObjectType(object) {
return Object.prototype.toString.call(object).slice(8, -1);
}
function _baseClone(target) {
if(target === null || typeof target !== 'object') {
return target
}
if(Array.isArray(target)) {
return Array.from(target).map(_baseClone)
}
switch(getObjectType(target)) {
case 'Date': return new Date(target)
case 'RegExp': return new RegExp(target)
case 'Set': {
const newSet = new Set()
target.forEach(item => {
newSet.add(_baseClone(item))
})
return newSet
}
}
const newRef = oldToNew.get(target)
if(newRef) {
return newRef;
}
// const newObject = Object.create(Object.getPrototypeOf(target))
// 改为深拷贝
const newObject = Object.create(_baseClone(Object.getPrototypeOf(target)))
oldToNew.set(target, newObject)
for(const key of Reflect.ownKeys(target)) {
newObject[key] = _baseClone(target[key])
}
return newObject;
}
return _baseClone(target)
}
直接这样改完之后,会沿着原型链向上一直拷贝到Object.prototype,甚至连Object.prototype也克隆了,这就导致了克隆后的对象使用instancetype of Object会返回false。