如何在 javascript 中深度克隆

如何深度克隆 JavaScript 对象?

我知道有很多基于框架的函数,比如 JSON.parse(JSON.stringify(o))$.extend(true, {}, o),但是我不想使用这样的框架。

创建深度克隆的最优雅或最有效的方法是什么。

我们确实关心像克隆数组这样的边缘情况,不打破原型链,处理自引用。

我们不关心是否支持复制 DOM 对象,因为 .cloneNode的存在就是为了这个原因。

因为我主要想在 node.js中使用 V8引擎的 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的状态。

现实世界不再需要这样的功能,这仅仅是学术兴趣。

181657 次浏览

这真的取决于你想克隆什么。这是一个真正的 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";
}
}
]
})

现在,让我们讨论一下开始克隆真实对象时可能遇到的问题。我现在说的是,你可以通过

var User = function(){}
var newuser = new User();

当然,您可以克隆它们,这不是问题,每个对象都公开构造函数属性,您可以使用它来克隆对象,但它并不总是有效。您也可以对这个对象执行简单的 for in,但是它的方向是一样的——麻烦。我还在代码中包含了克隆功能,但是它被 if( false )语句排除在外。

那么,为什么克隆会是一件痛苦的事情呢?首先,每个对象/实例都可能有一些状态。您永远不能确定您的对象没有私有变量,例如,如果是这种情况,通过克隆对象,您只是破坏了状态。

假设没有国家,没关系。那我们还有一个问题。通过“构造函数”方法进行克隆将给我们带来另一个障碍。这是一个参数依赖。你永远不能确定,创造这个物体的人,没有做过,某种

new User({
bike : someBikeInstance
});

如果是这种情况,那么您就不走运了,有些 BikeInstance 可能是在某个上下文中创建的,而这个上下文对于克隆方法来说是未知的。

那怎么办?您仍然可以执行 for in解决方案,并像对待普通对象文字一样对待这些对象,但是也许根本不克隆这些对象并只传递这个对象的引用是一种思想?

另一个解决方案是-您可以设置一个约定,即所有必须被克隆的对象都应该自己实现这一部分,并提供适当的 API 方法(如 cloneObject)。cloneNode为 DOM 所做的一些事情。

你来决定。

正如其他人在这个问题和类似问题上所指出的那样,在一般意义上,克隆一个“对象”在 JavaScript 中是可疑的。

然而,有一类对象,我称之为“数据”对象,即那些仅仅由 { ... }文字和/或简单的属性赋值构造的对象,或者从 JSON 反序列化的对象,对于这些对象,克隆是合理的。就在今天,我还想人为地将从服务器接收到的数据膨胀5倍,以测试大型数据集会发生什么情况,但是对象(数组)及其子对象必须是不同的对象才能正常工作。克隆使我能够这样做来增加我的数据集:

return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));

我经常克隆数据对象的另一个地方是将数据提交回主机,我希望在发送数据模型中的对象之前从该对象中剥离状态字段。例如,当对象被克隆时,我可能想从对象中去除以“ _”开头的所有字段。

这就是我最后编写的代码,包括支持数组和选择器来选择要克隆的成员(它使用“路径”字符串来确定上下文) :

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;
}

非常简单的方法,也许太简单了:

var cloned = JSON.parse(JSON.stringify(objectToClone));

Js 贡献库库有一个名为 快照的函数,它可以深度克隆一个对象

源代码片段:

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);

深度复制 Javascript 对象的 JSON.parse(JSON.stringify())组合是一种无效的黑客技术,因为它是针对 JSON 数据的。它不支持 undefinedfunction () {}的值,并且在将 Javascript 对象“字符串化”(编组)到 JSON 中时会简单地忽略它们(或 null它们)。

更好的解决方案是使用深度复制函数。下面的函数深度复制对象,不需要第三方库(jQuery、 LoDash 等)。

function copy(aObject) {
// Prevent undefined objects
// if (!aObject) return aObject;


let bObject = Array.isArray(aObject) ? [] : {};


let value;
for (const key in aObject) {


// Prevent self-references to parent object
// if (Object.is(aObject[key], aObject)) continue;
    

value = aObject[key];


bObject[key] = (typeof value === "object") ? copy(value) : value;
}


return bObject;
}

注意: 这段代码可以检查简单的自引用(取消 // Prevent self-references to parent object部分的注释) ,但是如果可能的话,还应该避免创建带有自引用的对象。请参阅: < a href = “ https://softwareengineering ering.stackexchange.com/questions/11856/whats-wrong-with-round-reference”> https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references

我注意到 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;
}

现在在 Web API 中有 结构化克隆,它也可以处理循环引用。


前一个答案

下面是 ES6函数,它也适用于具有循环引用的对象:

function deepClone(obj, hash = new WeakMap()) {
if (Object(obj) !== obj) return obj; // primitives
if (hash.has(obj)) return hash.get(obj); // cyclic reference
const result = obj instanceof Set ? new Set(obj) // See note about this!
: obj instanceof Map ? new Map(Array.from(obj, ([key, val]) =>
[key, deepClone(val, hash)]))
: obj instanceof Date ? new Date(obj)
: obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
// ... add here any specific treatment for other classes ...
// and finally a catch-all:
: obj.constructor ? new obj.constructor()
: Object.create(null);
hash.set(obj, result);
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 和 Maps 的键是有争议的: 这些键通常是原语(在这种情况下没有争议) ,但它们 可以也是对象。在这种情况下,问题就变成了: 是否应该克隆这些密钥?

有人可能会争辩说这是应该做的,这样一来,如果这些对象在拷贝中发生了变异,那么原始对象就不会受到影响,反之亦然。

另一方面,如果 Set/Map has是一个键,那么在原始文件和副本中都应该如此——至少在对它们进行任何更改之前。如果副本是一个 Set/Map,其中的键以前从未出现过(因为它们是在克隆过程中创建的) ,那将是很奇怪的: 对于任何需要知道给定对象是否是 Set/Map 中的键的代码来说,这肯定没有多大用处。

正如你所注意到的,我更倾向于第二种观点: Set 和 Maps 的键是 价值观(也许是 参考文献) ,它们应该保持不变。

这样的选择通常也会出现在其他(可能是自定义的)对象中。没有通用的解决方案,这很大程度上取决于克隆对象在特定情况下的行为。

这是我使用的深度克隆方法,我认为它 很好,希望你能给点建议

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;
}

这适用于数组、对象和基元。双重递归算法在两种遍历方法之间切换:

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);
}

我们可以利用递归进行深拷贝,它可以创建数组、对象、对象数组、对象函数的拷贝。 如果你愿意,你可以为其他类型的数据结构添加函数,比如 map 等。

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;
};

Lo-Dash ,现在是 下划线.js的一个超集,有两个深度克隆功能:

来自 作者的回答自己:

提供 lodash underscore版本是为了确保与 Underscore 的最新稳定版本的兼容性。

现实世界不再需要这样的功能,这仅仅是学术兴趣。

作为一个纯粹的练习,这是一个更加实用的方法。这是 @ tfmontague 的回答的一个扩展,作为 我建议在那里增加了一个保护块。但是,鉴于我觉得有必要使 ES6和功能化的所有东西,这里是我的皮条客版本。它使逻辑复杂化,因为您必须映射数组并减少对象,但它避免了任何变化。

const 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)))

我对所有答案的补充

function deepCopy(arr) {
if (typeof arr !== 'object') return arr
if (Array.isArray(arr)) return [...arr].map(deepCopy)
for (const prop in arr)
copy[prop] = deepCopy(arr[prop])
return copy
}

使用 ImmutableJS

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

或者 Loash/合并

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 }] }

这个,使用循环参考,对我有用

 //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));

下面的函数是深度克隆 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

Var newDate = new Date (this. oldDate) ; 我正在传递 oldDate 函数,并从 this. oldDate 生成 newDate,但它也在更改 this. oldDate。所以我使用了那个解决方案,它工作了。

当使用[ ... 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
}

避免使用此方法

let cloned = JSON.parse(JSON.stringify(objectToClone));

为什么? 这个方法将转换’函数,未定义’到 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]

尝试使用 deep Clone 函数

我的解决方案是深度克隆对象、数组和函数。

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的问候方法。


const person1 = {
name: 'John',
greet() {
return `HI, ${this.name}`
}
}
 

const person2 = JSON.parse(JSON.stringify(person1))
 

console.log(person2)  // { name: 'John' }


我们可以利用 StructuredClone ()实现深度克隆

const original = { name: "stack overflow" };




// Clone it
const clone = structuredClone(original);

你好,我只是想张贴我的答案,因为我认为它更可读。注意: 这不包括类,因为我不使用他们,但你可以很容易地添加一个条件

/** Copies any type of object/array of objects
* @param obj The object to be copied
* @param customKeys A list of keys that are to be excluded from deepCopy (optional)
*/
export function deepCopyObject(obj: any, customKeys?: Array<string|number|symbol>) {
if (obj == undefined)
return;
if (typeof obj !== 'object')
return obj;
if (typeof obj === 'function')
return obj;
    

const isArray = obj.length > -1;
if (isArray)
return copyArray(obj);
    

const isObjectDate = obj instanceof Date;
if(isObjectDate)
return new Date(obj);
    

const isDOM = obj.nodeType && typeof obj.cloneNode == "function";
if (isDOM)
return obj.cloneNode(true);
    

const isHtmlComponent = obj.$$typeof != undefined; // you can pass html/react components and maybe setup a custom function to copy them
if (isHtmlComponent)
return obj;


const newObject = <typeof obj>{};
const keys = Object.keys(obj);
keys.forEach((key: keyof (typeof obj)) => {
newObject[key] = copyKeysOfTypeObject(obj, key, customKeys);
})


const cantAccessObjectKeys = keys.lenght ==0; // ex: window.navigator
if (cantAccessObjectKeys)
return obj;
    

return newObject
}


function copyArray(arr: any) {
const newArr = new Array(0);
arr.forEach((obj: any) => {
newArr.push(deepCopyObject(obj));
})
return newArr;
}


function copyKeysOfTypeObject(obj: any, key: string | number | symbol, customKeys?: Array<string | number | symbol>) {
if (!key)
return;
if (customKeys && customKeys.includes(key))
return obj[key];
return deepCopyObject(obj[key]);
}

对象的深度克隆可以通过几种方式进行,但是每种方式都有自己的局限性,如下所述。因此,我建议您使用 结构化克隆算法。

  • 不会复制函数,日期,未定义和更多。

const obj = {
name: 'alpha',
printName: function() {
console.log(this.name);
}
};


console.log(JSON.parse(JSON.stringify(obj))); // function not copied

  • _.cloneDeep(object)-这是一个很好的选择,但是需要 loash。

const obj = {
name: 'alpha',
printName: function() {
console.log(this.name);
}
};


filteredArray = _.cloneDeep(obj);
console.log(filteredArray)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.2.1/lodash.min.js"></script>

  • structuredClone(object) - Browser Native API (It is good to use as JSON.parse() and JSON.stringify() do not serialize the circular object or things like Map, Set, Date, RegEx etc.)

const a = { x: 20, date: new Date() };
a.c = a;


console.log(structuredClone(a)); // { x: 20, date: <date object>, c: <circular ref> }


console.log(JSON.parse(JSON.stringify(a))); // throwing a TypeError

let obj1 = {
a: 100,
b: {
c: 200,
d: [1, 2, 3],
e: () => {}
}
}


function deepClone(obj) {
let newObj = {};


for (let key in obj) {
let val = obj[key];


if (val instanceof Array) {
newObj[key] = [...val]
} else if (typeof val === 'object') {
newObj[key] = deepClone(val)
} else {
newObj[key] = val;
}
}
return newObj;
}


obj2 = deepClone(obj1);


obj1.b.c = 300;


console.log(obj1);
console.log(obj2);

现在大多数浏览器都支持 structuredClone

它的主要限制是关于 DO NT 的应对功能,手动复制/移动需要一些额外的工作。

通过以后添加原型,我们至少可以以一种简单的方式复制 课程

const proto = Object.getPrototypeOf(object)
const newObject = structuredClone(object)
Object.setPrototypeOf(newObject, proto)