我有一个对象x。我想将其复制为对象y,这样对y的更改不会修改x。我意识到复制从内置JavaScript对象派生的对象会导致额外的、不需要的财产。这不是问题,因为我正在复制我自己的一个文字构造对象。
如何正确克隆JavaScript对象?
我有一个对象x。我想将其复制为对象y,这样对y的更改不会修改x。我意识到复制从内置JavaScript对象派生的对象会导致额外的、不需要的财产。这不是问题,因为我正在复制我自己的一个文字构造对象。
如何正确克隆JavaScript对象?
当前回答
互联网上的大多数解决方案都存在几个问题。所以我决定做一个跟进,包括为什么不应该接受接受的答案。
启动情况
我想深度复制一个Javascript对象及其所有子对象和它们的子对象等等。但由于我不是一个普通的开发人员,我的对象具有普通的财产、循环结构甚至嵌套对象。
因此,让我们先创建一个圆形结构和一个嵌套对象。
function Circ() {
this.me = this;
}
function Nested(y) {
this.y = y;
}
让我们把所有的东西放在一个名为a的对象中。
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 --> Object {
x: "b",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
b --> Object {
x: "b",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
现在让我们找到一个解决方案。
JSON
我第一次尝试是使用JSON。
var b = JSON.parse( JSON.stringify( a ) );
b.x = 'b';
b.nested.y = 'b';
不要在上面浪费太多时间,你会得到TypeError:将循环结构转换为JSON。
递归副本(接受的“答案”)
让我们来看看公认的答案。
function cloneSO(obj) {
// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;
// Handle Date
if (obj instanceof Date) {
var copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Array
if (obj instanceof Array) {
var copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = cloneSO(obj[i]);
}
return copy;
}
// Handle Object
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.");
}
看起来不错,呵呵?它是对象的递归副本,也处理其他类型,如Date,但这不是必需的。
var b = cloneSO(a);
b.x = 'b';
b.nested.y = 'b';
递归和循环结构不能很好地一起工作。。。RangeError:超出了最大调用堆栈大小
本地解决方案
在与同事争吵后,老板问我们发生了什么事,他在谷歌搜索后找到了一个简单的解决方案。它叫做Object.create。
var b = Object.create(a);
b.x = 'b';
b.nested.y = 'b';
这个解决方案不久前被添加到Javascript中,甚至可以处理循环结构。
console.log(a, b);
a --> Object {
x: "a",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
b --> Object {
x: "b",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
…你看,它不适用于内部的嵌套结构。
本地解决方案的polyfill
在旧浏览器中有一个用于Object.create的polyfill,就像IE 8一样。它有点像Mozilla推荐的,当然,它不是完美的,并且会导致与本机解决方案相同的问题。
function F() {};
function clonePF(o) {
F.prototype = o;
return new F();
}
var b = clonePF(a);
b.x = 'b';
b.nested.y = 'b';
我把F放在了范围之外,这样我们可以看看instanceof告诉我们什么。
console.log(a, b);
a --> Object {
x: "a",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
b --> F {
x: "b",
circ: Circ {
me: Circ { ... }
},
nested: Nested {
y: "b"
}
}
console.log(typeof a, typeof b);
a --> object
b --> object
console.log(a instanceof Object, b instanceof Object);
a --> true
b --> true
console.log(a instanceof F, b instanceof F);
a --> false
b --> true
问题与本机解决方案相同,但输出稍差。
更好(但不是完美)的解决方案
当我仔细研究时,我发现了一个与这个问题类似的问题(在Javascript中,当执行深度复制时,我如何避免循环,因为属性是“this”?),但有一个更好的解决方案。
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();
}
// else
o[gdcc] = function() { return result; }; // overwrite
if (o instanceof Array) {
result = [];
for (var i=0; i<o.length; i++) {
result[i] = cloneDR(o[i]);
}
} else {
result = {};
for (var prop in o)
if (prop != gdcc)
result[prop] = cloneDR(o[prop]);
else if (set)
result[prop] = cloneDR(cache);
}
if (set) {
o[gdcc] = cache; // reset
} else {
delete o[gdcc]; // unset again
}
return result;
}
var b = cloneDR(a);
b.x = 'b';
b.nested.y = 'b';
让我们看看输出。。。
console.log(a, b);
a --> Object {
x: "a",
circ: Object {
me: Object { ... }
},
nested: Object {
y: "a"
}
}
b --> Object {
x: "b",
circ: Object {
me: Object { ... }
},
nested: Object {
y: "b"
}
}
console.log(typeof a, typeof b);
a --> object
b --> object
console.log(a instanceof Object, b instanceof Object);
a --> true
b --> true
console.log(a instanceof F, b instanceof F);
a --> false
b --> false
需求是匹配的,但仍有一些较小的问题,包括将nested和circ的实例更改为Object。
共享一片叶子的树的结构不会被复制,它们将变成两片独立的叶子:
[Object] [Object]
/ \ / \
/ \ / \
|/_ _\| |/_ _\|
[Object] [Object] ===> [Object] [Object]
\ / | |
\ / | |
_\| |/_ \|/ \|/
[Object] [Object] [Object]
结论
最后一个使用递归和缓存的解决方案可能不是最好的,但它是对象的真正深度副本。它处理简单的财产、循环结构和嵌套对象,但在克隆时会弄乱它们的实例。
小提琴演奏家
其他回答
克隆对象的简单递归方法。也可以使用lodash.clone。
让克隆=(obj)=>{let obj2=Array.isArray(obj)?[] : {};for(设k为obj){obj2[k]=(obj[k]===“对象”的类型)?克隆(obj[k]):obj[k];}返回obj2;}让w={name:“Apple”,类型:[“Fuji”,“Gala”]};设x=克隆(w);w.name=“橙色”;w.types=[“Navel”];console.log(x);console.log(w);
根据Apple JavaScript编码指南:
// Create an inner object with a variable x whose default
// value is 3.
function innerObj()
{
this.x = 3;
}
innerObj.prototype.clone = function() {
var temp = new innerObj();
for (myvar in this) {
// this object does not contain any objects, so
// use the lightweight copy code.
temp[myvar] = this[myvar];
}
return temp;
}
// Create an outer object with a variable y whose default
// value is 77.
function outerObj()
{
// The outer object contains an inner object. Allocate it here.
this.inner = new innerObj();
this.y = 77;
}
outerObj.prototype.clone = function() {
var temp = new outerObj();
for (myvar in this) {
if (this[myvar].clone) {
// This variable contains an object with a
// clone operator. Call it to create a copy.
temp[myvar] = this[myvar].clone();
} else {
// This variable contains a scalar value,
// a string value, or an object with no
// clone function. Assign it directly.
temp[myvar] = this[myvar];
}
}
return temp;
}
// Allocate an outer object and assign non-default values to variables in
// both the outer and inner objects.
outer = new outerObj;
outer.inner.x = 4;
outer.y = 16;
// Clone the outer object (which, in turn, clones the inner object).
newouter = outer.clone();
// Verify that both values were copied.
alert('inner x is '+newouter.inner.x); // prints 4
alert('y is '+newouter.y); // prints 16
史蒂夫
Jan Turo的上述答案非常接近,由于兼容性问题,可能是在浏览器中使用的最佳选择,但这可能会导致一些奇怪的枚举问题。例如,执行:
for ( var i in someArray ) { ... }
在遍历数组元素后,将clone()方法赋给i。下面是一个避免枚举并适用于node.js的改编:
Object.defineProperty( Object.prototype, "clone", {
value: function() {
if ( this.cloneNode )
{
return this.cloneNode( true );
}
var copy = this instanceof Array ? [] : {};
for( var attr in this )
{
if ( typeof this[ attr ] == "function" || this[ attr ] == null || !this[ attr ].clone )
{
copy[ attr ] = this[ attr ];
}
else if ( this[ attr ] == this )
{
copy[ attr ] = copy;
}
else
{
copy[ attr ] = this[ attr ].clone();
}
}
return copy;
}
});
Object.defineProperty( Date.prototype, "clone", {
value: function() {
var copy = new Date();
copy.setTime( this.getTime() );
return copy;
}
});
Object.defineProperty( Number.prototype, "clone", { value: function() { return this; } } );
Object.defineProperty( Boolean.prototype, "clone", { value: function() { return this; } } );
Object.defineProperty( String.prototype, "clone", { value: function() { return this; } } );
这避免了使clone()方法可枚举,因为defineProperty()默认为false。
如果您有一个带有函数的对象,可以使用JSONfn来实现,请参见http://www.eslinstructor.net/jsonfn/.
var obj= {
name:'Marvin',
getName : function(){
return this.name;
}
}
var cobj = JSONfn.parse(JSONfn.stringify(obj));
要处理JSON.stringify无法处理的循环对象,可以引入一个名为JSOG的库,该库将任意图形序列化和反序列化为JSON格式。
var clone = JSOG.parse(JSOG.stringify(original));
尝试使用此技巧修补JSOG以进行克隆可能也很有趣(目前没有时间,但如果有人想尝试一下…):
序列化简单函数:
foo.f = function(a) { return a }
var stringForm = foo.f.toString() // "function (a) { return a }"
反序列化函数:
eval("foo.f = " + stringForm)
需要一些约定(可能是以属性的名称)来标识函数与正则字符串(可能是@func_f)。
当然,如果该函数调用第二个函数,则第二个功能将需要与原始函数一样存在。
然而,如果您要接受来自不可信来源的序列化表单,则上述内容非常危险,但接受来自不可靠来源的任何形式的函数都是危险的,因此如果您对克隆函数感兴趣,则必须已经建立了信任(或者您已经打算编写安全漏洞!)。
免责声明:我没有测试JSOG字符串/解析与JSON字符串/解析的速度,但它确实适用于我测试的简单(循环)对象。