JSON.parse(JSON.stringify(obj))实现深拷贝的缺点 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址;
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存用于存放复制的对象,使这个增加的指针指向这个新的内存;
JSON.parse(JSON.stringify(obj))深拷贝的问题
如果obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串。
如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。
如果obj里有函数、undefined,则序列化的结果会把函数、undefined丢失。
如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。
JSON.stringify()只能序列化对象的可枚举的自有属性 。如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。
如果对象中存在循环引用的情况也无法正确实现深拷贝。
不能处理循环引用
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 let person = {};Object .defineProperties (person, { 'name' : { configurable : true , enumerable : false , writable : true , value : 'test' }, 'age' : { configurable : true , enumerable : true , writable : true , value : 10 } }); let mySymbol = Symbol ();let obj = { data0 : 1 , data1 : new Date (), data2 : new RegExp ('\\W+' ), data3 : new Error ('1' ), data4 : undefined , data5 : function ( ) { console .log (1 ); }, data6 : NaN , data7 : person, data8 : { data_1 : 3 , data_2 : { sub_data_1 : 'heelo' }, data_3 : [1 , 2 , 3 ], }, [mySymbol]: { a : 'symbol' } } console .log (obj);console .log (JSON .parse (JSON .stringify (obj)));
为了解决上述JSON.parse(JSON.stringify(obj))实现深拷贝的缺点,可以通过递归实现一个深拷贝。
递归实现深拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function cloneDeep (target ) { if (typeof target === 'object' && target !== null ) { if (target.__proto__ .constructor === Date ) return new Date (target); if (target.__proto__ .constructor === RegExp ) return new RegExp (target); if (target.__proto__ .constructor === Map ) return new Map (target); if (target.__proto__ .constructor === Set ) return new Set (target); let res = new target.constructor ( ); for (let i of Reflect .ownKeys (target)) { res[i] = cloneDeep (target[i]); } return res; } else { return target; } }
用new target.constructor ()
构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承
测试结果:
循环引用 其实上面的递归实现深拷贝还存在一个问题:如果target.property = target
,这样会形成了一个环(看着更像是套娃)。
那这个问题怎么解决呢?最开始的想法是如果存在这种循环引用的情况,可以找个地方先暂存一下。克隆之前,先去找是否存在一样(引用地址相同)的引用类型,如果有直接返回,没有再克隆。那么Map就很容易想到啦。
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 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 70 71 72 73 let person = {};Object .defineProperties (person, { 'name' : { configurable : true , enumerable : false , writable : true , value : 'test' }, 'age' : { configurable : true , enumerable : true , writable : true , value : 10 } }); let mySymbol = Symbol ();let obj = { data0 : 1 , data1 : new Date (), data2 : new RegExp ('\\W+' ), data3 : new Error ('1' ), data4 : undefined , data5 : function ( ) { console .log (1 ); }, data6 : NaN , data7 : person, data8 : { data_1 : 3 , data_2 : { sub_data_1 : 'heelo' , sym : Symbol ('symm' ) }, data_3 : [1 , 2 , 3 ], }, [mySymbol]: { a : 'symbol' }, data10 : Symbol ('test' ), newMap : new Map ([['age' , 10 ]]), newSet : new Set ([1 , 2 , 3 ]) } obj.data9 = obj; console .log (obj);console .log (cloneDeep (obj));function cloneDeep (target, map = new Map () ) { if (typeof target === 'object' && target !== null ) { let res = new target.constructor ( ); if (map.get (target)) { return target; } map.set (target, res); if (target.__proto__ .constructor === Date ) return new Date (target); if (target.__proto__ .constructor === RegExp ) return new RegExp (target); if (target.__proto__ .constructor === Map ) return new Map (target); if (target.__proto__ .constructor === Set ) return new Set (target); for (let i of Reflect .ownKeys (target)) { res[i] = cloneDeep (target[i], map); } return res; } else { return target; } }
测试结果:
到这其实对日期对象(new Date()
)、正则对象(new RegExp()
)、Map
、Set
、Symbol
、function
、undefined
、null
、object
、Array
、new Error()
的拷贝。下面对代码结构优化一下,不希望出现那么多的if-else。
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 cloneDeep (target, map = new Map () ) { if (typeof target === 'object' && target !== null ) { let res = new target.constructor ( ); if (map.get (target)) { return target; } map.set (target, res); let tagType = target.__proto__ .constructor .name ; switch (tagType) { case 'Date' : return new Date (target); case 'RegExp' : return new RegExp (target); case 'Map' : return new Map (target); case 'Set' : return new Set (target); default : break ; } for (let i of Reflect .ownKeys (target)) { res[i] = cloneDeep (target[i], map); } return res; } else { return target; } }
参考文章