Let's talk about deep copy and shallow copy first
- Shallow copy
The so-called shallow copy is to copy only the outermost layer, and the references inside are still the same
// Shallow copy const a = { name: 'xiaoming', age: 23 } const b = {} for (let key in a){ b[key] = a[key] } console.log(b) // { name: 'xiaoming', age: 23 } console.log(b === a) // false console.log(b.name === a.name) // true
- Deep copy
Deep copy is to copy an object to another new variable, which points to a new heap memory address
// Deep copy function cloneDeep(target) { // ... dosomething for deep copy } const a = { name: 'xiaoming', age: 23 } const b = cloneDeep(a) console.log(b) // { name: 'xiaoming', age: 23} console.log(b === a) // false console.log(b.name === a.name) // false
- Deep copy implementation code
Common version
function cloneDeep(target) { return JSON.parse(JSON.stringify(target)) } const a = { name: 'xiaoming', age: 23 } const b = cloneDeep(a) console.log(b) // { name: 'xiaoming', age: 23 } console.log(b === a) // false
Although it is OK to use this method most of the time, there are still many problems in this way
1. If the field value in the object is undefined, the field will disappear directly after conversion
2. If an object has a field value of RegExp, the field value will become {} after conversion
3. If the field value of the object is NaN, + - Infinity, the field value becomes null after conversion
4. If the object has a ring reference, the conversion will directly report an error
Upgrade
function cloneDeep(target) { const temp = {} for (const key in target) { temp[key] = target[key] } return temp } const a = { name: 'xiaoming', age: 23 } const b = cloneDeep(a) console.log(b) // { name: 'xiaoming', age: 23 } console.log(b === a) // false
It looks good, but it's still not possible to optimize and recurse in case of deep layers.
function cloneDeep(target) { // Direct return of basic data type if (typeof target !== 'object') { return target } // Special handling of reference data types const temp = {} for (const key in target) { // recursion temp[key] = cloneDeep(target[key]) } return temp } const a = { name: 'xiaoming', age: 23, hobbies: { sports: 'swim', game: 'lol' } } const b = cloneDeep(a) console.log(b === a) // false
But now you can only copy objects, not arrays. We're optimizing.
function cloneDeep(target) { // Direct return of basic data type if (typeof target !== 'object') { return target } // Special handling of reference data types // Determine array or object const temp = Array.isArray(target) ? [] : {} for (const key in target) { // recursion temp[key] = cloneDeep(target[key]) } return temp } const a = { name: 'xiaoming', age: 23, hobbies: { sports: 'swim', game: 'lol' }, works: ['2020', '2021'] } const b = cloneDeep(a) console.log(b === a) // false
But it does not solve an important problem, the problem of ring reference.
- What is a circular reference
When an attribute in object 1 points to object 2 and an attribute in object 2 points to object 1, a circular reference will appear (of course, there is more than one case, but the principle is the same). Let's explain it through code.
let obj1 = {}; let obj2 = { b: obj1 }; obj1.a = obj2;
JSON.tostringify cannot serialize an infinitely referenced object into a JOSN string.
The following is an explanation of how JSON.stringify() converts the value to the corresponding JSON format:
- If the transformation value has a toJSON() method, the method defines what value will be serialized.
- Properties of non array objects cannot be guaranteed to appear in a serialized string in a specific order.
- The wrapper objects of Boolean value, number and string will be automatically converted to the corresponding original value during serialization.
- Undefined, arbitrary functions, and symbol values are ignored during serialization (when they appear in the property values of non array objects) or converted to null (when they appear in arrays). When functions and undefined are converted separately, undefined will be returned, such as JSON.stringify(function(){}) or JSON.stringify(undefined)
- Executing this method on objects that contain circular references (objects refer to each other to form an infinite loop) will throw an error.
- All attributes with symbol as the attribute key are completely ignored, even if they are forcibly specified in the replace parameter.
- Date date calls toJSON() to convert it into a string (the same as date. Toisstring()), so it will be treated as a string.
- Values in NaN and Infinity formats and null will be treated as null.
- Other types of objects, including Map/Set/WeakMap/WeakSet, serialize only enumerable properties.
Of course, the solution to the above can also be written in this way
let obj1 = { }; let obj2 = { b: obj1 }; obj1.a = obj2; let c = JSON.decycle(obj1); JSON.stringify(c)
In combination with the above problems, upgrade the deep copy
function cloneDeep(target, map = new Map()) { // Direct return of basic data type if (typeof target !== 'object') { return target } // Special handling of reference data types // Determine array or object const temp = Array.isArray(target) ? [] : {} if (map.get(target)) { // Return directly if it already exists return map.get(target) } // If it does not exist, it is set for the first time map.set(target, temp) for (const key in target) { // recursion temp[key] = cloneDeep(target[key], map) } return temp } const a = { name: 'xiaoming', age: 23, hobbies: { sports: 'swim', game: 'lol' }, works: ['2020', '2021'] } a.key = a // Ring reference const b = cloneDeep(a) console.log(b === a) // false
It seems OK, but it can't realize all the requirements. It can only realize some common arrays, objects and so on.
But in fact, reference data types are not only arrays and objects. We also have to solve the problem of copying the following reference types. How to judge the respective types of each reference data type? You can use Object.prototype.toString.call()
This will return all data types as follows;
type | ||
Map | Object.prototype.toString.call(new Map()) | [object Map] |
Set | Object.prototype.toString.call(new Set()) | [object Set] |
Array | Object.prototype.toString.call([]) | [object Array] |
Object | Object.prototype.toString.call({}) | [object Object] |
Symbol | Object.prototype.toString.call(Symbol()) | [object Symbol] |
RegExp | Object.prototype.toString.call(new RegExp()) | [object RegExp] |
Function | Object.prototype.toString.call(function() {}) | [object Function] |
The tree types are divided into two categories: the first is ergodic and the second is non ergodic.
// Traversable type const mapTag = '[object Map]'; const setTag = '[object Set]'; const arrayTag = '[object Array]'; const objectTag = '[object Object]'; // Non traversable type const symbolTag = '[object Symbol]'; const regexpTag = '[object RegExp]'; const funcTag = '[object Function]'; // Store traversable types in an array const canForArr = ['[object Map]', '[object Set]','[object Array]', '[object Object]'] // There will be an array of non traversable types const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]'] // Function for judging type function checkType(target) { return Object.prototype.toString.call(target) } // temp to determine the reference type function checkTemp(target) { const c = target.constructor return new c() }
Summarize the two methods of judging
// Method of copying Function function cloneFunction(func) { const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); if (func.prototype) { const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if (body) { if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { return eval(funcString); } } // Method of copying Symbol function cloneSymbol(targe) { return Object(Symbol.prototype.valueOf.call(targe)); } // How to copy RegExp function cloneReg(targe) { const reFlags = /\w*$/; const result = new targe.constructor(targe.source, reFlags.exec(targe)); result.lastIndex = targe.lastIndex; return result; }
Combine the final version
function cloneDeep(target, map = new Map()) { // Get type const type = checkType(target) // Direct return of basic data type if (!canForArr.concat(noForArr).includes(type)) return target // Judge Function, RegExp, Symbol if (type === funcTag) return cloneFunction(target) if (type === regexpTag) return cloneReg(target) if (type === symbolTag) return cloneSymbol(target) // Special handling of reference data types const temp = checkTemp(target) if (map.get(target)) { // Return directly if it already exists return map.get(target) } // If it does not exist, it is set for the first time map.set(target, temp) // Processing Map types if (type === mapTag) { target.forEach((value, key) => { temp.set(key, cloneDeep(value, map)) }) return temp } // Process Set type if (type === setTag) { target.forEach(value => { temp.add(cloneDeep(value, map)) }) return temp } // Processing data and objects for (const key in target) { // recursion temp[key] = cloneDeep(target[key], map) } return temp } const a = { name: 'xiaoming', age: 23, hobbies: { sports: 'swim', game: 'lol' }, works: ['2020', '2021'] map: new Map([['aaa', 0001], ['bbb', 0002]]), set: new Set([1, 2, 3]), func: (do) => `${do}!`, sym: Symbol(1111), reg: new RegExp(/oooooooooo/g), } a.key = a // Ring reference const b = cloneDeep(a) console.log(b) console.log(b === a) // false