- Published on
如何正确地克隆JavaScript对象?
- Authors
- Name
在对 JavaScript 对象进行深度拷贝时,网上的许多解决方案都存在一些问题。因此,我决定写一篇文章来详细讨论这个问题,并说明为什么某些被广泛接受的答案其实并不理想。
起始情况
我想要 深度复制 一个包含各种复杂结构的 JavaScript Object
,这个对象可能包含普通属性、循环引用以及嵌套对象。首先,我们定义一个包含循环引用和嵌套对象的结构。
function Circ() {
this.me = this;
}
function Nested(y) {
this.y = y;
}
var a = {
x: 'a',
circ: new Circ(),
nested: new Nested('a')
};
接着,我们将 a
复制到变量 b
中,并对其进行修改:
var b = a;
b.x = 'b';
b.nested.y = 'b';
console.log(a, b);
输出结果显示两个对象是同一个引用:
a, b -->
Object {
x: "b",
circ: Circ { me: [Circular] },
nested: Nested { y: "b" }
}
JSON方案
许多人会尝试使用 JSON
来深度复制:
var b = JSON.parse(JSON.stringify(a));
b.x = 'b';
b.nested.y = 'b';
然而,这样会导致 TypeError: Converting circular structure to JSON
错误,因为 JSON
不支持循环引用。
递归复制(被广泛接受的“答案”)
我们来看一下一个常见的递归复制方法:
function cloneSO(obj) {
if (null == obj || "object" != typeof obj) return obj;
if (obj instanceof Date) {
var 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] = cloneSO(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
var copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = cloneSO(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
var b = cloneSO(a);
b.x = 'b';
b.nested.y = 'b';
然而,这种方法在处理循环引用时会导致 RangeError: Maximum call stack size exceeded
错误。
原生解决方案
经过讨论后,我们发现了一个简单的原生解决方案—— Object.create
:
var b = Object.create(a);
b.x = 'b';
b.nested.y = 'b';
console.log(a, b);
这种方法虽然能处理循环引用,但在处理嵌套对象时会失效。
原生解决方案的Polyfill
对于较老的浏览器(如IE 8),我们可以使用 Object.create
的 polyfill:
function F() {};
function clonePF(o) {
F.prototype = o;
return new F();
}
var b = clonePF(a);
b.x = 'b';
b.nested.y = 'b';
console.log(a, b);
这种方法同样存在与原生解决方案相似的问题,并且生成的对象类型有差异。
更好的(但还不完美)解决方案
我们可以参考 这个问题,并实现一个更好的深度复制方法:
function cloneDR(o) {
const gdcc = "__getDeepCircularCopy__";
if (o !== Object(o)) {
return o; // primitive value
}
var set = gdcc in o,
cache = o[gdcc],
result;
if (set && typeof cache == "function") {
return cache();
}
o[gdcc] = function() { return result; };
result = Array.isArray(o) ? [] : {};
for (var prop in o) {
if (prop != gdcc) {
result[prop] = cloneDR(o[prop]);
}
}
if (set) {
o[gdcc] = cache;
} else {
delete o[gdcc];
}
return result;
}
var b = cloneDR(a);
b.x = 'b';
b.nested.y = 'b';
console.log(a, b);
这种方法能真实地深度复制对象,尽管在某些实例的类型上有一些小问题。
结论
最后一种方法使用递归和缓存技术,能够正确处理循环引用和嵌套对象,是一个真正的深度复制方案。尽管它会在某些实例类型上出现问题,但仍然是目前较好的解决方案。
您可以通过这个 jsfiddle 进行进一步测试。