深拷贝JS中的任何对象都不是一件容易的事情,你将会遇到这样的问题,从object的原型中,错误的选择应该留在原型上而不是拷贝到新实例上的属性。举个例子,你添加了一个clone
的方法到Object.prototype
,正如其他答案描述的,你需要显式地跳过该属性。但是如果其他额外的方法或者中间原型也添加到了Object.prototype
,而你不知道。在这种情况下,你将会拷贝你不需要的属性,所以你需要用hasOwnProperty
方法检查不可预见的、非局部的属性。
除了不可枚举的属性之外,你将会遇到更难的一个问题,就是当你拷贝一个拥有隐藏属性的对象。举个例子,函数的prototype
是隐藏的,对象原型的引用__proto__
属性也是隐藏的,通过for/in
的迭代方法将不能拷贝源对象上的这些属性。我认为Firefox的JS解释器的__proto__
属性是比较特殊的,可能跟其他的浏览器有点不同,但是你可以想到,不是一切都是可枚举的。如果你知道属性的名字,那你就能够拷贝这个属性,但是我不知道怎么去自动发现这些隐藏的属性。
另一个障碍是寻找一个优雅的解决方案,正确的设置原型的继承。如果你的源对象是一个Object
,那么简单的用{}
创建一个普通的对象也会工作。但是如果源对象上的原型是某些Object
的后代,那么你使用hasOwnProperty
方法过滤的时候,或者在原型链开始的地方是不能枚举的,将会跳过原型,丢失一些额外的成员。一个解决方法是调用源对象上的constructor
的属性得到初始化的拷贝对象,然后拷贝属性,不过你仍然不能得到不可枚举的属性。例如,一个Date
对象存储的数据是隐藏的。
function clone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr];
}
}
return copy;
}
var d1 = new Date();
// 等五秒钟
var start = (new Date()).getTime();
while ((new Date().getTime()) - start < 5000);
var d2 = clone(d1);
console.log('d1=' + d1.toString() + 'd2=' + d2.toString());
// d1=Tue Aug 30 2016 10:19:59 GMT+0800 (CST)d2=Tue Aug 30 2016 10:20:04 GMT+0800 (CST)
d2的值将会比d1的值大5秒钟。有一个让Date
类型与另一个Date
类型相同的办法,就是调用setTime
方法,但是这只是对Date
类而言的。我认为这不是一个万无一失的方法,我很乐意是错的!
当我必须实现一个通用的深拷贝的方法时,我最终还是妥协了,假设我只有纯Object、Array、Date、String、Number或者Boolean类型需要拷贝。最后3个类型是不可变的,所以我能够执行浅拷贝而不用担心它改变了。我进一步假设任何包含对象或数组的元素也将是这6个简单类型其中的一个,这可以用下面的代码来实现:
function clone(obj) {
var copy;
// 处理3个简单的类型, null 或者 undefined
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
if (obj instanceof Array) {
var copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = clone(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
var copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = clone(obj[attr]);
}
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
上面的方法完全能够在我提到的那6个简单类型中工作,只要对象和数组中的数据形成一个树状结构,也就是说,在1个对象中没有多于1个的对相同数据的引用。例如:
// 这是可以克隆的
var tree = {
"left": { "left": null, "right": null, "data": 3 },
"right": null,
"data": 8
};
// 这样也可以工作,但是你会得到2份内部节点,而不是2个引用相同的副本
var directedAcyclicGraph = {
"left" : { "left" : null, "right" : null, "data" : 3 },
"data" : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];
// 这种情况因为无限的递归,会导致堆栈溢出
var cylicGraph = {
"left" : { "left" : null, "right" : null, "data" : 3 },
"data" : 8
};
cylicGraph["right"] = cylicGraph;
这个clone的方法不能处理所有的JS对象,但已经能满足大部分的需求了,只要你不把所有的工作的丢给它就可以了。