我有一个对象x。我想将其复制为对象y,这样对y的更改不会修改x。我意识到复制从内置JavaScript对象派生的对象会导致额外的、不需要的财产。这不是问题,因为我正在复制我自己的一个文字构造对象。
如何正确克隆JavaScript对象?
我有一个对象x。我想将其复制为对象y,这样对y的更改不会修改x。我意识到复制从内置JavaScript对象派生的对象会导致额外的、不需要的财产。这不是问题,因为我正在复制我自己的一个文字构造对象。
如何正确克隆JavaScript对象?
当前回答
如果您正在使用TypeScript,需要支持较旧的web浏览器(因此无法使用Object.assign),并且没有使用内置有克隆方法的库,那么您可以在几行代码中使自己成为组合助手。它结合了对象,如果只有一个,就克隆它。
/** Creates a new object that combines the properties of the specified objects. */
function combine(...objs: {}[]) {
const combined = {};
objs.forEach(o => Object.keys(o).forEach(p => combined[p] = o[p]));
return combined;
}
其他回答
如果您有一个带有函数的对象,可以使用JSONfn来实现,请参见http://www.eslinstructor.net/jsonfn/.
var obj= {
name:'Marvin',
getName : function(){
return this.name;
}
}
var cobj = JSONfn.parse(JSONfn.stringify(obj));
如果对象中没有循环依赖关系,我建议使用其他答案之一或jQuery的复制方法,因为它们看起来都很有效。
如果存在循环依赖关系(即,两个子对象彼此链接),那么你就有点糟糕了,因为(从理论角度)没有办法优雅地解决这个问题。
表演
今天2020.04.30我在MacOs High Sierra v10.13.6上对Chrome v81.0、Safari v13.1和Firefox v75.0上选择的解决方案进行了测试。
我专注于复制DATA(具有简单类型字段的对象,而不是方法等)的速度。解决方案A-I只能进行浅层复制,解决方案J-U可以进行深层复制。
浅层副本的结果
解决方案{…obj}(A)在chrome和firefox上速度最快,在safari上速度中等基于Object.assign(B)的解决方案在所有浏览器上都很快jQuery(E)和lodash(F,G,H)解决方案是中等/相当快的解决方案JSON.parse/stringify(K)非常慢解决方案D和U在所有浏览器上都很慢
深度复制的结果
解决方案Q在所有浏览器上都是最快的jQuery(L)和lodash(J)是中速的解决方案JSON.parse/stringify(K)非常慢解决方案U在所有浏览器上都是最慢的lodash(J)和1000级深对象的Chrome上的U崩溃解决方案
细节
对于选择的解决方案:A.BC(我的)DEFGH我JKLMNOPQRSTU我进行了4次测试
浅小:具有10个非嵌套字段的对象-可以在此处运行浅大:具有1000个非嵌套字段的对象-可以在此处运行deep-small:具有10级嵌套字段的对象-可以在此处运行deep-big:具有1000级嵌套字段的对象-可以在此处运行
测试中使用的对象显示在下面的代码段中
let obj_ShallowSmall={字段0:假,字段1:真,字段2:1,字段3:0,字段4:空,字段5:[],字段6:{},field7:“text7”,字段8:“text8”,}让obj_DepSmall={级别0:{级别1:{级别2:{级别3:{级别4:{级别5:{级别6:{第7级:{级别8:{第9级:[[[[[[[[]],}}}}}}}}},};让obj_ShallowBig=数组(1000).fill(0).reduce((a,c,i)=>(a['field'+i]=getField(i),a),{});让obj_DepBig=genDeepObject(1000);// ------------------//显示对象// ------------------console.log('obj_ShallowSmall:',JSON.stringify(obj_SharlowSmall));console.log('obj_DepSmall:',JSON.stringify(obj_DeepSmall));console.log('obj_ShallowBig:',JSON.stringify(obj_SharlowBig));console.log('obj_DepBig:',JSON.stringify(obj_DeepBig));// ------------------//助手// ------------------函数getField(k){设i=k%10;如果(i==0)返回false;如果(i==1)返回真;如果(i==2)返回k;如果(i==3)返回0;如果(i==4)返回null;如果(i==5)返回[];如果(i==6)返回{};如果(i>=7)返回“text”+k;}函数genDeepObject(N){//生成:{level0:{level1:{…levelN:{end:[[[…N次…['abc']…]]}}}}…}}让obj={};设o=obj;设arr=[];设a=arr;for(设i=0;i<N;i++){o['level'+i]={};o=o['level'+i];设aa=[];a.推(aa);a=aa;}a[0]=“公元前”;o['end']=arr;返回obj;}
下面的代码片段展示了经过测试的解决方案,并显示了它们之间的差异
函数A(obj){返回{…obj}}函数B(obj){return Object.assign({},obj);}函数C(obj){return Object.keys(obj).reduce((a,c)=>(a[c]=obj[c],a),{})}函数D(obj){让copyOfObject={};Object.defineProperties(copyOfObject,Object.getOwnPropertyDescriptors(obj));返回copyOfObject;}函数E(obj){return jQuery.exextend({},obj)//浅}函数F(obj){return _.clone(obj);}函数G(obj){return _.clone(obj,true);}函数H(obj){返回扩展({},obj);}函数I(obj){如果(null==obj||“对象”!=obj类型)返回obj;var copy=obj.constructor();for(obj中的var属性){如果(obj.hasOwnProperty(attr))copy[attr]=obj[attr];}返回副本;}函数J(obj){return _.cloneDeep(obj,true);}函数K(obj){返回JSON.parse(JSON.stringify(obj));}函数L(obj){return jQuery.exextend(true,{},obj)//deep}函数M(obj){如果(obj==null | |类型(obj)!='对象')返回obj;var temp=新建obj.constructor();for(obj中的var键)temp[key]=M(obj[key]);返回温度;}函数N(obj){设EClone=函数(obj){var newObj=(数组的obj实例)?[] : {};for(对象中的变量i){如果(i=='EClone')继续;如果(obj[i]&&类型obj[i]==“对象”){newObj[i]=EClone(obj[i]);}否则newObj[i]=obj[i]}返回newObj;};返回EClone(obj);};函数O(obj){如果(obj==null||typeof obj!=“object”)返回obj;如果(obj.constructor!=对象&&obj.constructionr!=数组)返回obj;如果(obj.constructor==日期| | obj.constructionr==RegExp | | obc.constructor==函数||obj.structor==字符串||obj.structor==数字||obc.structor==布尔值)返回新的obj.constructor(obj);let to=new obj.constructor();for(obj中的var名称){to[name]=typeof to[name]==“undefined”?O(obj[名称],null):到[名称];}返回;}函数P(obj){函数克隆(目标,源){for(输入源代码){//使用getOwnPropertyDescriptor而不是source[key]来防止触发setter/getter。let descriptor=Object.getOwnPropertyDescriptor(源,键);if(字符串的descriptor.value实例){target[key]=新字符串(descriptor.value);}else if(数组的descriptor.value实例){target[key]=克隆([],descriptor.value);}else if(descriptor.value对象实例){let prototype=Reflect.getPrototypeOf(descriptor.value);let cloneObject=clone({},descriptor.value);Reflect.setPrototypeOf(克隆对象,原型);target[key]=克隆对象;}其他{Object.defineProperty(目标、键、描述符);}}let prototype=Reflect.getPrototypeOf(源代码);Reflect.setPrototypeOf(目标,原型);回归目标;}返回克隆({},obj);}函数Q(obj){var复制;//处理3个简单类型,以及null或undefined如果(null==obj||“对象”!=obj类型)返回obj;//处理日期if(日期的obj实例){copy=新日期();copy.setTime(obj.getTime());返回副本;}//句柄数组if(数组的obj实例){copy=[];对于(变量i=0,len=obj.length;i<len;i++){copy[i]=Q(obj[i]);}返回副本;}//句柄对象if(对象的obj实例){copy={};for(obj中的var属性){如果(obj.hasOwnProperty(attr))copy[attr]=Q(obj.[attr]);}返回副本;}throw new Error(“无法复制obj!不支持其类型。”);}函数R(obj){const gdcc=“__getDeepCircularCopy__”;if(obj!==对象(obj)){返回obj;//原始值}var set=obj中的gdcc,缓存=obj[gdcc],后果if(设置缓存类型==“函数”){return cache();}//其他obj[gdcc]=函数(){return result;};//覆盖if(数组的obj实例){结果=[];对于(var i=0;i<obj.length;i++){结果[i]=R(obj[i]);}}其他{结果={};for(obj中的var属性)if(prop!=gdcc)result[prop]=R(obj[prop]);else if(设置)result[prop]=R(缓存);}if(设置){obj[gdcc]=缓存;//重置}其他{删除对象[gdcc];//再次取消设置}返回结果;}函数S(obj){const cache=new WeakMap();//新旧参考地图函数副本(对象){if(对象类型!=='对象'||对象==空||HTMLElement的对象实例)返回对象;//原始值或HTMLElementif(日期的对象实例)return new Date().setTime(object.getTime());if(RegExp的对象实例)返回新的RegExp(object.source,object.flags);if(cache.has(对象))返回cache.get(对象);const result=数组的对象实例?[] : {};cache.set(对象,结果);//在递归开始之前存储对对象的引用if(数组的对象实例){for(对象的常量){result.push(复制(o));}返回结果;}const keys=对象.keys(对象);for(键的常量键)result[key]=复制(对象[key]);返回结果;}返回副本(obj);}
下面是Chrome对于浅大型对象的示例结果
(以下内容主要是@Maciej Bukowski、@A.Levy、@Jan Turoğ、@Redu的回答,以及@LeviRoberts、@RobG的评论的整合,非常感谢他们!!!)
深度复制?-对!(大部分);浅拷贝?-不!(代理除外)。
我真诚欢迎大家来测试clone()。此外,defineProp()设计用于轻松快速地(重新)定义或复制任何类型的描述符。
作用
function clone(object) {
/*
Deep copy objects by value rather than by reference,
exception: `Proxy`
*/
const seen = new WeakMap()
return clone(object)
function clone(object) {
if (object !== Object(object)) return object /*
—— Check if the object belongs to a primitive data type */
if (object instanceof Node) return object.cloneNode(true) /*
—— Clone DOM trees */
let _object // The clone of object
switch (object.constructor) {
case Array:
case Object:
_object = cloneObject(object)
break
case Date:
_object = new Date(+object)
break
case Function:
_object = copyFn(object)
break
case RegExp:
_object = new RegExp(object)
break
default:
switch (Object.prototype.toString.call(object.constructor)) {
// // Stem from:
case "[object Function]":
switch (object[Symbol.toStringTag]) {
case undefined:
_object = cloneObject(object) // `class`
break
case "AsyncFunction":
case "GeneratorFunction":
case "AsyncGeneratorFunction":
_object = copyFn(object)
break
default:
_object = object
}
break
case "[object Undefined]": // `Object.create(null)`
_object = cloneObject(object)
break
default:
_object = object // `Proxy`
}
}
return _object
}
function cloneObject(object) {
if (seen.has(object)) return seen.get(object) /*
—— Handle recursive references (circular structures) */
const _object = Array.isArray(object)
? []
: Object.create(Object.getPrototypeOf(object)) /*
—— Assign [[Prototype]] for inheritance */
seen.set(object, _object) /*
—— Make `_object` the associative mirror of `object` */
Reflect.ownKeys(object).forEach(key =>
defineProp(_object, key, { value: clone(object[key]) }, object)
)
return _object
}
}
function copyPropDescs(target, source) {
Object.defineProperties(target,
Object.getOwnPropertyDescriptors(source)
)
}
function convertFnToStr(fn) {
let fnStr = String(fn)
if (fn.name.startsWith("[")) // isSymbolKey
fnStr = fnStr.replace(/\[Symbol\..+?\]/, '')
fnStr = /^(?!(async )?(function\b|[^{]+?=>))[^(]+?\(/.test(fnStr)
? fnStr.replace(/^(async )?(\*)?/, "$1function$2 ") : fnStr
return fnStr
}
function copyFn(fn) {
const newFn = new Function(`return ${convertFnToStr(fn)}`)()
copyPropDescs(newFn, fn)
return newFn
}
function defineProp(object, key, descriptor = {}, copyFrom = {}) {
const { configurable: _configurable, writable: _writable }
= Object.getOwnPropertyDescriptor(object, key)
|| { configurable: true, writable: true }
const test = _configurable // Can redefine property
&& (_writable === undefined || _writable) // Can assign to property
if (!test || arguments.length <= 2) return test
const basisDesc = Object.getOwnPropertyDescriptor(copyFrom, key)
|| { configurable: true, writable: true } // Custom…
|| {}; // …or left to native default settings
["get", "set", "value", "writable", "enumerable", "configurable"]
.forEach(attr =>
descriptor[attr] === undefined &&
(descriptor[attr] = basisDesc[attr])
)
const { get, set, value, writable, enumerable, configurable }
= descriptor
return Object.defineProperty(object, key, {
enumerable, configurable, ...get || set
? { get, set } // Accessor descriptor
: { value, writable } // Data descriptor
})
}
//测试
const obj0 = {
u: undefined,
nul: null,
t: true,
num: 9,
str: "",
sym: Symbol("symbol"),
[Symbol("e")]: Math.E,
arr: [[0], [1, 2]],
d: new Date(),
re: /f/g,
get g() { return 0 },
o: {
n: 0,
o: { f: function (...args) { } }
},
f: {
getAccessorStr(object) {
return []
.concat(...
Object.values(Object.getOwnPropertyDescriptors(object))
.filter(desc => desc.writable === undefined)
.map(desc => Object.values(desc))
)
.filter(prop => typeof prop === "function")
.map(String)
},
f0: function f0() { },
f1: function () { },
f2: a => a / (a + 1),
f3: () => 0,
f4(params) { return param => param + params },
f5: (a, b) => ({ c = 0 } = {}) => a + b + c
}
}
defineProp(obj0, "s", { set(v) { this._s = v } })
defineProp(obj0.arr, "tint", { value: { is: "non-enumerable" } })
obj0.arr[0].name = "nested array"
let obj1 = clone(obj0)
obj1.o.n = 1
obj1.o.o.g = function g(a = 0, b = 0) { return a + b }
obj1.arr[1][1] = 3
obj1.d.setTime(+obj0.d + 60 * 1000)
obj1.arr.tint.is = "enumerable? no"
obj1.arr[0].name = "a nested arr"
defineProp(obj1, "s", { set(v) { this._s = v + 1 } })
defineProp(obj1.re, "multiline", { value: true })
console.log("\n\n" + "-".repeat(2 ** 6))
console.log(">:>: Test - Routinely")
console.log("obj0:\n ", JSON.stringify(obj0))
console.log("obj1:\n ", JSON.stringify(obj1))
console.log()
console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)
console.log()
console.log("obj0\n ",
".arr.tint:", obj0.arr.tint, "\n ",
".arr[0].name:", obj0.arr[0].name
)
console.log("obj1\n ",
".arr.tint:", obj1.arr.tint, "\n ",
".arr[0].name:", obj1.arr[0].name
)
console.log()
console.log("Accessor-type descriptor\n ",
"of obj0:", obj0.f.getAccessorStr(obj0), "\n ",
"of obj1:", obj1.f.getAccessorStr(obj1), "\n ",
"set (obj0 & obj1) .s :", obj0.s = obj1.s = 0, "\n ",
" → (obj0 , obj1) ._s:", obj0._s, ",", obj1._s
)
console.log("—— obj0 has not been interfered.")
console.log("\n\n" + "-".repeat(2 ** 6))
console.log(">:>: Test - More kinds of functions")
const fnsForTest = {
f(_) { return _ },
func: _ => _,
aFunc: async _ => _,
async function() { },
async asyncFunc() { },
aFn: async function () { },
*gen() { },
async *asyncGen() { },
aG1: async function* () { },
aG2: async function* gen() { },
*[Symbol.iterator]() { yield* Object.keys(this) }
}
console.log(Reflect.ownKeys(fnsForTest).map(k =>
`${String(k)}:
${fnsForTest[k].name}-->
${String(fnsForTest[k])}`
).join("\n"))
const normedFnsStr = `{
f: function f(_) { return _ },
func: _ => _,
aFunc: async _ => _,
function: async function() { },
asyncFunc: async function asyncFunc() { },
aFn: async function () { },
gen: function* gen() { },
asyncGen: async function* asyncGen() { },
aG1: async function* () { },
aG2: async function* gen() { },
[Symbol.iterator]: function* () { yield* Object.keys(this) }
}`
const copiedFnsForTest = clone(fnsForTest)
console.log("fnsForTest:", fnsForTest)
console.log("fnsForTest (copied):", copiedFnsForTest)
console.log("fnsForTest (normed str):", eval(`(${normedFnsStr})`))
console.log("Comparison of fnsForTest and its clone:",
Reflect.ownKeys(fnsForTest).map(k =>
[k, fnsForTest[k] === copiedFnsForTest[k]]
)
)
console.log("\n\n" + "-".repeat(2 ** 6))
console.log(">:>: Test - Circular structures")
obj0.o.r = {}
obj0.o.r.recursion = obj0.o
obj0.arr[1] = obj0.arr
obj1 = clone(obj0)
console.log("obj0:\n ", obj0)
console.log("obj1:\n ", obj1)
console.log("Clear obj0's recursion:",
obj0.o.r.recursion = null, obj0.arr[1] = 1
)
console.log(
"obj0\n ",
".o.r:", obj0.o.r, "\n ",
".arr:", obj0.arr
)
console.log(
"obj1\n ",
".o.r:", obj1.o.r, "\n ",
".arr:", obj1.arr
)
console.log("—— obj1 has not been interfered.")
console.log("\n\n" + "-".repeat(2 ** 6))
console.log(">:>: Test - Classes")
class Person {
constructor(name) {
this.name = name
}
}
class Boy extends Person { }
Boy.prototype.sex = "M"
const boy0 = new Boy
boy0.hobby = { sport: "spaceflight" }
const boy1 = clone(boy0)
boy1.hobby.sport = "superluminal flight"
boy0.name = "one"
boy1.name = "neo"
console.log("boy0:\n ", boy0)
console.log("boy1:\n ", boy1)
console.log("boy1's prototype === boy0's:",
Object.getPrototypeOf(boy1) === Object.getPrototypeOf(boy0)
)
工具书类
Object.create()| MDN对象.defineProperties()| MDN财产的可枚举性和所有权| MDNTypeError:循环对象值|MDN
使用的语言技巧
有条件地将道具添加到对象
使用jQuery,可以使用extend进行浅层复制:
var copiedObject = jQuery.extend({}, originalObject)
对copiedObject的后续更改不会影响originalObject,反之亦然。
或制作深度副本:
var copiedObject = jQuery.extend(true, {}, originalObject)