How to Deep clone in javascript
如何深入克隆一个javascript对象?
我知道有很多基于框架的函数,比如
创建深度克隆的最优雅或最有效的方法是什么?
我们关心的是像克隆阵列这样的边缘情况,而不是破坏原型链,处理自引用。
我们不关心是否支持复制dom对象之类的,因为
因为我主要想在
[编辑]
在任何人提出建议之前,让我先提一下,通过从对象继承原型来创建拷贝和克隆拷贝之间有着明显的区别。前者把原型链搞得一团糟。
[进一步编辑]
在阅读了你的答案之后,我发现克隆整个物体是一个非常危险和困难的游戏。以下面的基于闭包的对象为例
1 2 3 4 5 6 7 8 9 10 11 12 | 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解析器的情况下无法更改
在现实世界中不应该再需要这样的功能了。这只是学术上的兴趣。
非常简单的方法,可能太简单了:
1 | var cloned = JSON.parse(JSON.stringify(objectToClone)); |
这真的取决于你想克隆什么。这是一个真正的JSON对象还是JavaScript中的任何对象?如果你想做任何克隆,它可能会给你带来一些麻烦。哪一个麻烦?我将在下面解释它,但首先是一个代码示例,它克隆对象文本、任何原语、数组和DOM节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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"; } } ] }) |
现在,我们来谈谈克隆真正的对象时可能会遇到的问题。我现在说的是,你通过做一些类似的事情
1 2 | var User = function(){} var newuser = new User(); |
当然,您可以克隆它们,这不是问题,每个对象都公开构造函数属性,并且您可以使用它来克隆对象,但它并不总是有效的。您也可以在这个对象上执行简单的
那么,为什么克隆会是一种痛苦呢?首先,每个对象/实例都可能有某种状态。你永远不能确定你的对象没有私有变量,如果是这样的话,通过克隆对象,你只会破坏状态。
想象一下没有状态,没关系。那我们还有另一个问题。通过"构造器"方法克隆将给我们带来另一个障碍。这是一个参数依赖关系。你永远无法确定,创造这个物体的人,没有,某种
1 2 3 | new User({ bike : someBikeInstance }); |
如果是这样的话,你就不走运了,可能是在某个上下文中创建了某个bikeInstance,而该上下文对于clone方法是未知的。
那么该怎么办呢?您仍然可以执行
另一个解决方案是-您可以设置一个约定,必须克隆的所有对象都应该自己实现这一部分,并提供适当的API方法(如CloneObject)。
你决定。
The
JSON.parse(JSON.stringify()) combination to deep copy Javascript objects is an ineffective hack, because JSON does not support values ofundefined andfunction () {} , and thereforeJSON.stringify will ignore those sections of code, when"stringifying" (marshalling) the Javascript object into JSON.
以下功能将深度复制对象,不需要第三方库(jquery、lodash等)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } |
下面是一个ES6函数,它也适用于具有循环引用的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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 |
如何处理集合和地图的键是有争议的:这些键通常是原始的(在这种情况下没有争论),但它们也可以是对象。在这种情况下,问题变成了:应该克隆那些密钥吗?
有人可能会说应该这样做,这样如果那些对象在副本中发生了变异,那么原始对象就不会受到影响,反之亦然。
另一方面,如果一个集合/映射
正如您所注意到的,我更倾向于第二种观点:集合和映射的键是应该保持不变的值(可能是引用)。
这些选择通常也会出现在其他(可能是自定义)对象上。没有通用的解决方案,这在很大程度上取决于克隆对象在特定情况下的行为。
js contrib库库中有一个名为snapshot的函数,可以深度克隆对象。
源代码段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; } |
一旦库链接到您的项目,只需使用
1 | _.snapshot(object); |
Lo Dash现在是一个underline.js的超集,它有两个深度克隆函数:
_.cloneDeep(object) _.cloneDeepWith(object, (val) => {if(_.isElement(val)) return val.cloneNode(true)}) 第二个参数是一个函数,它被调用来生成克隆的值。
从作者本人的回答来看:
lodash underscore build is provided to ensure compatibility with the latest stable version of Underscore.
我认为这是我使用的深度克隆方法太好了,希望你能提出建议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } |
正如其他人在这个和类似的问题上所指出的,在一般意义上,克隆一个"对象"在JavaScript中是可疑的。
但是,有一类对象,我称之为"数据"对象,即那些简单地从
1 | return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta)); |
另一个我经常结束克隆数据对象的地方是将数据提交回主机,在那里我想在发送数据之前从数据模型中的对象中除去状态字段。例如,在克隆对象时,我可能希望从对象中去掉以"u"开头的所有字段。
这是我最后编写的用于一般性执行此操作的代码,包括支持数组和选择要克隆哪些成员的选择器(使用"path"字符串确定上下文):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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; } |
最简单合理的深度克隆解决方案是,假设根对象为非空且没有成员选择:
1 2 3 4 5 6 7 8 9 10 | 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; } |
我注意到map需要特殊的处理,因此在这个线程中有了所有的建议,代码将是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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; } |
使用不变的JS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 |
或住宿/合并
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 }] } |
我对所有答案的补充
1 2 3 4 5 6 7 8 | 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 } |
There should be no real world need for such a function anymore. This is mere academic interest.
作为纯粹的练习,这是一种更实用的方法。这是@tfmontague答案的扩展,我建议在这里添加一个防护栏。但鉴于我觉得有必要使用ES6和所有功能,这里是我的皮条客版本。它使逻辑复杂化,因为您必须在数组上映射并在对象上减少,但它避免了任何突变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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))) |
我们可以利用递归来制作deepcopy。它可以创建数组、对象、对象数组、具有函数的对象的副本。如果需要,可以为其他类型的数据结构(如map等)添加函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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; }; |
这适用于数组、对象和基元。在两种遍历方法之间切换的双递归算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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); } |