浅拷贝和深拷贝
先用简单的两句话概括深拷贝和浅拷贝的区别吧。
浅复制:只将对象的各个属性进行一层复制,因此对于引用数据类型而言复制的是对象地址,导致了“牵一发而动全身”。
深复制:递归复制了所有层级,复制引用数据类型时会开辟新的栈空间,因此两个对象指向了两个不同的地址。
浅拷贝的实现
这里顺道简单罗列下浅拷贝的方法吧,具体的就不过多介绍了。
- concat 方法浅拷贝数组。
- slice 方法浅拷贝数组。
- Object.assign。
- … 展开运算符。
- 手动实现。
function shallowClone(target) {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
} else {
return target;
}
}
深拷贝的实现
JSON.parse+JSON.stringify 实现
我们实现深拷贝一个对象/数组的时候,除了通过递归去拷贝对象/数组中的每一个引用类型外,使用 JSON.parse() + JSON.stringify()
组合也可以实现深拷贝。不过这存在以下几个缺陷:
- 如果对象里面有
Date
对象,则转换后的结果中,时间将只是字符串的形式。而不是对象。
let obj = [new Date()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(typeof obj[0]); // object
console.log(typeof obj1[0]); // string
- 如果对象里有
RegExp
、Error
、Set
,Map
对象,则拷贝后只会得到空对象。
let obj = [new RegExp()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj); // [ /(?:)/ ]
console.log(obj1); // [ {} ]
- 在对象中遇到
undefined
、function
和symbol
时会自动将其忽略,在数组中则会返回null
。
- 在数组中
let obj = [function(){console.log(1)}, undefined, null, Symbol()];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj); // [ [Function], undefined, null, Symbol() ]
console.log(obj1); // [ null, null, null, null ]
- 在对象中
let obj = {
a: function(){console.log(1)},
b: undefined,
c: null,
d: Symbol()
};
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj); // { a: [Function: a], b: undefined, c: null, d: Symbol() }
console.log(obj1); // { c: null }
- 如果对象里有
NaN
、Infinity
和-Infinity
,则拷贝后的结果会变成null
。
let obj = [NaN, Infinity, -Infinity];
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj); // [ NaN, Infinity, -Infinity ]
console.log(obj1); // [ null, null, null ]
- 只能拷贝对象的可枚举的自有属性。如果对象中的某个属性是由构造函数生成的,则深拷贝后会丢弃该属性的
constructor
。
function Person(name) {
this.name = name;
}
let person = new Person("dangosky");
let obj = {date: person};
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj); // { date: Person { name: 'liai' } }
console.log(obj1); // { date: { name: 'liai' } }
- 如果对象中存在循环引用的情况也无法正确实现深拷贝。
const a = {
val: 2
};
a.target = a;
递归实现
再来看看如何用递归逐层拷贝对象属性以实现深拷贝。
function deepCopy(obj) {
if(typeof obj !== 'object') return obj;
let res = obj.constructor === Array ? [] : {};
for(let key in obj) {
if(typeof obj[key] === 'object' && obj[key] !== null) {
res[key] = deepCopy(obj[key]);
}
else {
res[key] = obj[key];
}
}
return res;
}
var obj = {a:1, arr: [{t: 1}, {t: 2}], b: null, c: undefined};
let obj1 = deepCopy(obj);
obj1.arr[1].t = 20;
console.log(obj); // { a: 1, arr: [ { t: 1 }, { t: 2 } ], b: null, c: undefined }
console.log(obj1); // { a: 1, arr: [ { t: 1 }, { t: 20 } ], b: null, c: undefined }
通过上述 deepCopy
递归实现深拷贝已经能正常工作了,但也存在几个缺陷:
- 无法拷贝
Date
,RegExp
,Error
、Set
,Map
对象。 - 无法拷贝对象属性中的
constructor
。 - 无法解决循环引用的问题。
优化(最终版本)
下面我们来对 deepCopy
优化一下。
- 解决循环引用的问题。只需要记录拷贝过的对象,并在拷贝一个对象之前先判断下该对象是否已经拷贝过就行了。这里可以借助 Map 来实现。
function deepCopy(obj, map = new WeakMap()) {
if (map.get(obj)) {
return obj;
}
if(typeof obj !== 'object') return obj;
let res = obj.constructor === Array ? [] : {};
// 标记 obj 已经拷贝过了
map.set(obj, true);
for(let key in obj) {
if(typeof obj[key] === 'object' && obj[key] !== null) {
res[key] = deepCopy(obj[key], map);
}
else {
res[key] = obj[key];
}
}
return res;
}
要注意的是,上述代码里使用 WeakMap
而不是 Map
。这是因为 Map
上的 key
和 Map
构成了强引用关系,而 WeakMap
则是弱引用。举个例子说明下什么是强引用和弱引用。(详情可见阮一峰的 ES6 教程)
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
上面代码里 arr
使用到了 e1
和 e2
两个对象。如果是强引用的话,则当不需要使用这两个对象的时候,还需要手动删除 arr
对这两个对象的引用(arr[0] = null; arr[1] = null
),否则垃圾回收机制就不会释放 e1
和 e2
占用的内存。但如果是弱引用的话,垃圾回收机制则不将该引用考虑在内。因此只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
考虑到当前这个场景,如果 Map
引用的对象已经不需要用到了,那么这些对象就可以被回收了。使用 Map
因为是强引用,所以还需要手动去释放 Map
对这些对象的引用。但如果使用 WeakMap
的话则是弱引用,WeakMap
对其使用的对象不会计入引用范围内。(WeakMap
的键名只能是 Object
)
- 解决
Date
,RegExp
,Error
、Set
,Map
等数据类型的拷贝问题。需要判断要拷贝对象的数据类型,并获取该数据类型的构造器,以此来构造出一个新的数据。对于Date
,RegExp
,Error
可以直接利用它们的指构造出一个新的数据,对于Set
和Map
就需要再遍历它们的元素并递归拷贝。
function deepCopy(obj, map = new WeakMap()) {
if (map.get(obj)) {
return obj;
}
// 如果 obj 只是基本类型的话,就直接返回
if(typeof obj !== 'object' || obj === null) return obj;
const objType = Object.prototype.toString.call(obj);
// 根据 obj 的数据类型获取到它的构造函数
const constructorFn = Object.getPrototypeOf(obj).constructor;
// 根据构造器创建不同的数据类型,并注意需要传递 obj 为参数。如果是 Date、Error 等数据类型才可以获取到这个值
const res = new constructorFn(obj);
// 标记 obj 已经拷贝过了
map.set(obj, true);
if (objType === "[object Array]" || objType === "[object Object]") {
for(let key in obj) {
// 因为 in 方法会遍历到 obj 的原型连上,所以需要判 key 是不是 obj 自己的属性
if (obj.hasOwnProperty(key)) {
res[key] = deepCopy(obj[key], map);
}
}
} else if (objType === "[object Map]") {
obj.forEach((item, key) => {
res.set(deepCopy(key), deepCopy(item));
})
} else if (objType === "[object Set]") {
obj.forEach(item => {
res.add(deepCopy(item));
})
}
return res;
}
/* Test */
const obj = {
'a': 1,
'b': false,
'c': 'dangosky',
'd': [1, 2 ,3],
'e': { 'name': 'dangosky' },
'f': undefined,
'g': null,
'h': new Map([ ['key', 'size']]),
'i': new Set([1, 2, 3]),
'j': new Date(1245),
'k': new Error('error'),
'l': new RegExp(/dangosky/g),
'm': function() {console.log(1)},
'n': () => {console.log(2)}
}
const res = deepCopy(obj);
console.log(obj);
console.log(res);