如何深度克隆Javascript对象?
我知道基于JSON.parse(JSON.stringify(o))
和$.extend(true, {}, o)
之类的框架有各种各样的功能,但我不想使用这样的框架。
创建深度克隆的最优雅或最有效的方法是什么。
我们关心像克隆数组这样的边缘情况。不破坏原型链,处理自我参考。
我们不关心支持复制DOM对象等因为.cloneNode
因为这个原因而存在。
由于我主要想在node.js
中使用深度克隆,因此使用V5引擎的ES5功能是可以接受的。
[编辑]
在有人建议之前让我提一下,通过原型继承对象和克隆来创建副本之间存在明显的区别。前者使原型链变得混乱。
[进一步编辑]
在阅读完答案之后,我发现了一个令人讨厌的发现,即克隆整个物体是一个非常危险和困难的游戏。例如,以下基于闭包的对象
var o = (function() {
var magic = 42;
var magicContainer = function() {
this.get = function() { return magic; };
this.set = function(i) { magic = i; };
}
return new magicContainer;
}());
var n = clone(o); // how to implement clone to support closures
有没有办法编写克隆对象的克隆函数,在克隆时具有相同的状态,但如果不在JS中编写JS解析器,则无法改变o
的状态。
不再需要这样的功能。这仅仅是学术兴趣。
答案 0 :(得分:130)
非常简单的方式,也许太简单了:
var cloned = JSON.parse(JSON.stringify(objectToClone));
答案 1 :(得分:58)
这真的取决于你想要克隆的东西。这是一个真正的JSON对象还是JavaScript中的任何对象?如果你想做任何克隆,它可能会让你遇到麻烦。哪个麻烦?我将在下面解释它,但首先是一个克隆对象文字,任何基元,数组和DOM节点的代码示例。
function clone(item) {
if (!item) { return item; } // null, undefined values check
var types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = clone( child );
});
} else if (typeof item == "object") {
// testing that this is DOM
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} else if (!item.prototype) { // check that this is a literal
if (item instanceof Date) {
result = new Date(item);
} else {
// it is an object literal
result = {};
for (var i in item) {
result[i] = clone( item[i] );
}
}
} else {
// depending what you would like here,
// just keep the reference, or create new object
if (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
var copy = clone({
one : {
'one-one' : new String("hello"),
'one-two' : [
"one", "two", true, "four"
]
},
two : document.createElement("div"),
three : [
{
name : "three-one",
number : new Number("100"),
obj : new function() {
this.name = "Object test";
}
}
]
})
现在,让我们谈谈开始克隆REAL对象时可能遇到的问题。我现在正在谈论关于你通过做
之类的东西创建的对象var User = function(){}
var newuser = new User();
当然你可以克隆它们,这不是问题,每个对象都暴露构造函数属性,你可以用它来克隆对象,但它并不总是有效。你也可以在这个对象上做简单的for in
,但是它朝着同一个方向 - 麻烦。我还在代码中包含了克隆功能,但它被if( false )
语句排除。
那么,为什么克隆可能是一种痛苦?好吧,首先,每个对象/实例可能都有一些状态。你永远无法确定你的对象没有私有变量,如果是这种情况,通过克隆对象,你就可以打破状态。
想象一下没有国家,没关系。然后我们还有另一个问题。通过“构造函数”方法克隆将给我们带来另一个障碍。这是一个参数依赖。你永远无法确定,创建这个对象的人没有做某种
new User({
bike : someBikeInstance
});
如果是这种情况,你运气不好,someBikeInstance可能是在某些上下文中创建的,并且克隆方法的上下文是未知的。
那该怎么办?您仍然可以执行for in
解决方案,并将此类对象视为普通对象文字,但也许根本不克隆此类对象,只是传递此对象的引用?
另一个解决方案是 - 您可以设置一个约定,即必须克隆的所有对象应该自己实现此部分并提供适当的API方法(如cloneObject)。 cloneNode
为DOM做的事情。
你决定。
答案 2 :(得分:30)
深度复制Javascript对象的
JSON.parse(JSON.stringify())
组合是一种无效的黑客攻击,因为JSON不支持undefined
和function () {}
的值,因此JSON.stringify
将忽略这些部分代码,当"字符串化" (编组)将Javascript对象转换为JSON。
以下函数将深层复制对象,并且不需要第三方库(jQuery,LoDash等)。
function copy(aObject) {
if (!aObject) {
return aObject;
}
let v;
let bObject = Array.isArray(aObject) ? [] : {};
for (const k in aObject) {
v = aObject[k];
bObject[k] = (typeof v === "object") ? copy(v) : v;
}
return bObject;
}
答案 3 :(得分:11)
这是一个ES6函数,它也适用于具有循环引用的对象:
function deepClone(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj; // primitives
if (obj instanceof Set) return new Set(obj); // See note about this!
if (hash.has(obj)) return hash.get(obj); // cyclic reference
const result = obj instanceof Date ? new Date(obj)
: obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
: obj.constructor ? new obj.constructor()
: Object.create(null);
hash.set(obj, result);
if (obj instanceof Map)
Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash)) );
return Object.assign(result, ...Object.keys(obj).map (
key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
var p = {
data: 1,
children: [{
data: 2,
parent: null
}]
};
p.children[0].parent = p;
var q = deepClone(p);
console.log(q.children[0].parent.data); // 1
如何处理集合和映射的键是有争议的:这些键通常是原语(在这种情况下没有争议),但它们 也可以是对象。在这种情况下,问题就变成了:那些密钥应该被克隆吗?
有人可能会认为应该这样做,以便如果这些对象在副本中发生变异,原始对象就不会受到影响,反之亦然。
另一方面,人们希望如果一个Set / Map has
一个键,这在原始和副本中都应该是真的 - 至少在对它们中的任何一个进行任何更改之前。如果副本是一个具有以前从未发生的密钥的Set / Map(因为它们是在克隆过程中创建的),那将是奇怪的:对于任何需要知道给定对象是否为a的代码来说,这肯定不是很有用是否设置了地图集。
正如您所注意到的,我更多的是第二种观点:集合和地图的键是值(可能是引用)应该保持不变。
此类选择通常也会与其他(可能是自定义)对象一起出现。没有通用的解决方案,因为在很大程度上取决于克隆对象在特定情况下的行为方式。
答案 4 :(得分:10)
Underscore.js contrib library库有一个名为snapshot的函数,可深度克隆对象
源代码摘录:
snapshot: function(obj) {
if(obj == null || typeof(obj) != 'object') {
return obj;
}
var temp = new obj.constructor();
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = _.snapshot(obj[key]);
}
}
return temp;
}
将库链接到项目后,只需使用
调用该函数_.snapshot(object);
答案 5 :(得分:3)
正如其他人已经注意到这个和类似的问题一样,克隆一个“对象”,在一般意义上,在JavaScript中是可疑的。
然而,有一类对象,我称之为“数据”对象,即那些简单地从{ ... }
文字和/或简单属性赋值构建的对象,或者从JSON反序列化的对象,它们是合理的。克隆。就在今天,我想人为地将从服务器接收的数据膨胀5倍以测试大型数据集会发生什么,但是对象(数组)及其子代必须是不同的对象才能使事物正常运行。克隆允许我这样做来乘以我的数据集:
return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));
我经常最终克隆数据对象的另一个地方是将数据提交回主机,我想在发送之前从数据模型中的对象中删除状态字段。例如,我可能想要在克隆时从对象中删除以“_”开头的所有字段。
这是我最后编写的代码,通常用于支持数组和选择器来选择要克隆的成员(使用“path”字符串来确定上下文):
function clone(obj,sel) {
return (obj ? _clone("",obj,sel) : obj);
}
function _clone(pth,src,sel) {
var ret=(src instanceof Array ? [] : {});
for(var key in src) {
if(!src.hasOwnProperty(key)) { continue; }
var val=src[key], sub;
if(sel) {
sub+=pth+"/"+key;
if(!sel(sub,key,val)) { continue; }
}
if(val && typeof(val)=='object') {
if (val instanceof Boolean) { val=Boolean(val); }
else if(val instanceof Number ) { val=Number (val); }
else if(val instanceof String ) { val=String (val); }
else { val=_clone(sub,val,sel); }
}
ret[key]=val;
}
return ret;
}
最简单的合理深度克隆解决方案,假设一个非空的根对象且没有成员选择:
function clone(src) {
var ret=(src instanceof Array ? [] : {});
for(var key in src) {
if(!src.hasOwnProperty(key)) { continue; }
var val=src[key];
if(val && typeof(val)=='object') { val=clone(val); }
ret[key]=val;
}
return ret;
}
答案 6 :(得分:3)
Lo-Dash,现在是Underscore.js的超集,有几个深层克隆函数:
_.cloneDeep(object)
_.cloneDeepWith(object, (val) => {if(_.isElement(val)) return val.cloneNode(true)})
第二个参数是一个被调用以产生克隆值的函数。
来自answer of the author他自己:
提供
lodash underscore
版本以确保与最新稳定版本的Underscore兼容。
答案 7 :(得分:3)
这是我使用的深度克隆方法,我认为 太好了,希望你提出建议
function deepClone (obj) {
var _out = new obj.constructor;
var getType = function (n) {
return Object.prototype.toString.call(n).slice(8, -1);
}
for (var _key in obj) {
if (obj.hasOwnProperty(_key)) {
_out[_key] = getType(obj[_key]) === 'Object' || getType(obj[_key]) === 'Array' ? deepClone(obj[_key]) : obj[_key];
}
}
return _out;
}
答案 8 :(得分:2)
以下功能是深度克隆javascript对象的最有效方法。
function deepCopy(obj){
if (!obj || typeof obj !== "object") return obj;
var retObj = {};
for (var attr in obj){
var type = obj[attr];
switch(true){
case (type instanceof Date):
var _d = new Date();
_d.setDate(type.getDate())
retObj[attr]= _d;
break;
case (type instanceof Function):
retObj[attr]= obj[attr];
break;
case (type instanceof Array):
var _a =[];
for (var e of type){
//_a.push(e);
_a.push(deepCopy(e));
}
retObj[attr]= _a;
break;
case (type instanceof Object):
var _o ={};
for (var e in type){
//_o[e] = type[e];
_o[e] = deepCopy(type[e]);
}
retObj[attr]= _o;
break;
default:
retObj[attr]= obj[attr];
}
}
return retObj;
}
var obj = {
string: 'test',
array: ['1'],
date: new Date(),
object:{c: 2, d:{e: 3}},
function: function(){
return this.date;
}
};
var copyObj = deepCopy(obj);
console.log('object comparison', copyObj === obj); //false
console.log('string check', copyObj.string === obj.string); //true
console.log('array check', copyObj.array === obj.array); //false
console.log('date check', copyObj2.date === obj.date); //false
console.log('object check', copyObj.object === obj.object); //false
console.log('function check', copyObj.function() === obj.function()); //true
答案 9 :(得分:2)
let cloned = JSON.parse(JSON.stringify(objectToClone));
为什么?此方法会将'function,undefined'转换为null
const myObj = [undefined, null, function () {}, {}, '', true, false, 0, Symbol];
const IsDeepClone = JSON.parse(JSON.stringify(myObj));
console.log(IsDeepClone); //[null, null, null, {…}, "", true, false, 0, null]
尝试使用deepClone函数。上面有几个
答案 10 :(得分:1)
我注意到Map应该需要特殊处理,因此在这个帖子中有所有建议,代码将是:
function deepClone( obj ) {
if( !obj || true == obj ) //this also handles boolean as true and false
return obj;
var objType = typeof( obj );
if( "number" == objType || "string" == objType ) // add your immutables here
return obj;
var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
if( obj instanceof Map )
for( var key of obj.keys() )
result.set( key, deepClone( obj.get( key ) ) );
for( var key in obj )
if( obj.hasOwnProperty( key ) )
result[key] = deepClone( obj[ key ] );
return result;
}
答案 11 :(得分:0)
这适用于数组,对象和基元。双重递归算法,在两种遍历方法之间切换:
const deepClone = (objOrArray) => {
const copyArray = (arr) => {
let arrayResult = [];
arr.forEach(el => {
arrayResult.push(cloneObjOrArray(el));
});
return arrayResult;
}
const copyObj = (obj) => {
let objResult = {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
objResult[key] = cloneObjOrArray(obj[key]);
}
}
return objResult;
}
const cloneObjOrArray = (el) => {
if (Array.isArray(el)) {
return copyArray(el);
} else if (typeof el === 'object') {
return copyObj(el);
} else {
return el;
}
}
return cloneObjOrArray(objOrArray);
}
答案 12 :(得分:0)
我们可以利用递归来制作deepCopy。它可以创建数组,对象,对象数组,对象数据的副本。 如果需要,可以为地图等其他类型的数据结构添加功能。
function deepClone(obj) {
var retObj;
_assignProps = function(obj, keyIndex, retObj) {
var subType = Object.prototype.toString.call(obj[keyIndex]);
if(subType === "[object Object]" || subType === "[object Array]") {
retObj[keyIndex] = deepClone(obj[keyIndex]);
}
else {
retObj[keyIndex] = obj[keyIndex];
}
};
if(Object.prototype.toString.call(obj) === "[object Object]") {
retObj = {};
for(key in obj) {
this._assignProps(obj, key, retObj);
}
}
else if(Object.prototype.toString.call(obj) == "[object Array]") {
retObj = [];
for(var i = 0; i< obj.length; i++) {
this._assignProps(obj, i, retObj);
}
};
return retObj;
};
答案 13 :(得分:0)
不再需要这样的功能。这仅仅是学术兴趣。
纯粹是一种锻炼,这是一种更有效的锻炼方式。它是@tfmontague's answer的扩展,I'd suggested在那里添加了一个保护块。但看到我觉得被迫使用ES6并将所有东西功能化,这是我的拉皮条版本。它使逻辑变得复杂,因为你必须映射数组并减少对象,但它避免了任何突变。
function cloner(x) {
const recurseObj = x => typeof x === 'object' ? cloner(x) : x
const cloneObj = (y, k) => {
y[k] = recurseObj(x[k])
return y
}
// Guard blocks
// Add extra for Date / RegExp if you want
if (!x) {
return x
}
if (Array.isArray(x)) {
return x.map(recurseObj)
}
return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
null,
[],
{},
[1,2,3],
[1,2,3, null],
[1,2,3, null, {}],
[new Date('2001-01-01')], // FAIL doesn't work with Date
{x:'', y: {yx: 'zz', yy: null}, z: [1,2,3,null]},
{
obj : new function() {
this.name = "Object test";
}
} // FAIL doesn't handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))
答案 14 :(得分:0)
我对所有答案的补充
deepCopy = arr => {
if (typeof arr !== 'object') return arr
if(arr.pop) return [...arr].map(deepCopy)
const copy = {}
for (let prop in arr)
copy[prop] = deepCopy(arr[prop])
return copy
}
答案 15 :(得分:0)
import { fromJS } from 'immutable';
// An object we want to clone
let objA = {
a: { deep: 'value1', moreDeep: {key: 'value2'} }
};
let immB = fromJS(objA); // Create immutable Map
let objB = immB.toJS(); // Convert to plain JS object
console.log(objA); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
console.log(objB); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
// objA and objB are equalent, but now they and their inner objects are undependent
console.log(objA === objB); // false
console.log(objA.a === objB.a); // false
console.log(objA.moreDeep === objB.moreDeep); // false
import merge from 'lodash/merge'
var objA = {
a: [{ 'b': 2 }, { 'd': 4 }]
};
// New deeply cloned object:
merge({}, objA );
// We can also create new object from several objects by deep merge:
var objB = {
a: [{ 'c': 3 }, { 'e': 5 }]
};
merge({}, objA , objB ); // Object { a: [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
答案 16 :(得分:0)
这个使用循环引用对我有用
//a test-object with circular reference :
var n1 = { id:0, text:"aaaaa", parent:undefined}
var n2 = { id:1, text:"zzzzz", parent:undefined }
var o = { arr:[n1,n2], parent:undefined }
n1.parent = n2.parent = o;
var obj = { a:1, b:2, o:o }
o.parent = obj;
function deepClone(o,output){
if(!output) output = {};
if(o.______clone) return o.______clone;
o.______clone = output.______clone = output;
for(var z in o){
var obj = o[z];
if(typeof(obj) == "object") output[z] = deepClone(obj)
else output[z] = obj;
}
return output;
}
console.log(deepClone(obj));
答案 17 :(得分:0)
var newDate =新日期(this.oldDate); 我正在将oldDate传递给函数,并从this.oldDate生成newDate,但是它也在更改this.oldDate。因此我使用了该解决方案,并且有效。
答案 18 :(得分:0)
此解决方案将避免在使用[... target]或{... target}时出现递归问题
function shallowClone(target) {
if (typeof a == 'array') return [...target]
if (typeof a == 'object') return {...target}
return target
}
/* set skipRecursion to avoid throwing an exception on recursive references */
/* no need to specify refs, or path -- they are used interally */
function deepClone(target, skipRecursion, refs, path) {
if (!refs) refs = []
if (!path) path = ''
if (refs.indexOf(target) > -1) {
if (skipRecursion) return null
throw('Recursive reference at ' + path)
}
refs.push(target)
let clone = shallowCopy(target)
for (i in target) target[i] = deepClone(target, refs, path + '.' + i)
return clone
}
答案 19 :(得分:0)
我的解决方案是深度克隆对象,数组和函数。
let superClone = (object) => {
let cloning = {};
Object.keys(object).map(prop => {
if(Array.isArray(object[prop])) {
cloning[prop] = [].concat(object[prop])
} else if(typeof object[prop] === 'object') {
cloning[prop] = superClone(object[prop])
} else cloning[prop] = object[prop]
})
return cloning
}
示例
let obj = {
a: 'a',
b: 'b',
c: {
deep: 'try and copy me',
d: {
deeper: 'try me again',
callDeeper() {
return this.deeper
}
},
arr: [1, 2, 3]
},
hi() {
return this.a
}
};
const cloned = superClone(obj)
obj.a = 'A'
obj.c.deep = 'i changed'
obj.c.arr = [45,454]
obj.c.d.deeper = 'i changed'
console.log(cloned) // unchanged object
如果您的对象包含方法不使用JSON进行深度克隆,则JSON深度克隆不会克隆方法。
如果您对此进行查看,则对象person2
仅克隆名称,而不克隆person1
的greet方法。
const person1 = {
name: 'John',
greet() {
return `HI, ${this.name}`
}
}
const person2 = JSON.parse(JSON.stringify(person1))
console.log(person2) // { name: 'John' }