Last active
May 22, 2024 18:45
-
-
Save lukakostic/582b664b6081581b6eb5c39ad814026f to your computer and use it in GitHub Desktop.
JS Circular reference DeepCopy and LinearizedDeepCopy (can be jsoned and back)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
DeepCopy and LinearizedDeepCopy which can handle circular references. | |
LinearizedDeepCopy can be jsonified and back yet keep circular references. | |
(For use see comments below the functions) | |
// these dont clone functions or keep class(constructor) info!!! | |
// so its like JSON.parse(JSON.stringify(obj)) but faster(?) and can do circular references. | |
*/ | |
function deepCopy(obj, map = undefined, reverseMap = undefined) { | |
if (map === undefined) { | |
map = new WeakMap(); | |
reverseMap = new WeakMap(); | |
} | |
function register(original, clone) { | |
map.set(original, clone); | |
reverseMap.set(clone, original); | |
return clone; | |
} | |
let clone = map.get(obj); //clone obj (from map) | |
if (clone !== undefined) return clone; //already processed | |
if (reverseMap.get(obj) !== undefined) return obj; //obj is already my clone | |
let cpy = (k, from = obj) => clone[k] = deepCopy(from[k], map, reverseMap); | |
if (Array.isArray(obj)) { | |
let props = Object.getOwnPropertyNames(obj); | |
clone = register(obj, [...obj]); | |
for (let i = 0; i < obj.length; i++) { | |
let idx = props.indexOf(i.toString()); | |
props.slice(idx, idx + 1); //remove processed | |
cpy(i); | |
} | |
props.forEach(p => cpy(p)); //process unprocessed | |
//have to handle the indexes (<length) | |
//as well as user-added properties to array object. | |
} else if (typeof obj == 'object') { | |
clone = register(obj, {}); | |
for (var k in obj) cpy(k); | |
} else { | |
clone = obj; //copy | |
} | |
return clone; | |
} | |
function linearDeepCopy(obj, map = undefined, reverseMap = undefined, indexedObjs = undefined) { | |
if (map === undefined) { | |
map = new WeakMap(); | |
reverseMap = new WeakMap(); | |
indexedObjs = []; | |
} | |
function register(original, clone) { | |
let idx = indexedObjs.push(clone) - 1; | |
let idxObj = { "!objRefIdx!": idx }; // unique name so we cant mistake it for user objects | |
map.set(original, idxObj); | |
reverseMap.set(clone, original); | |
return [clone, idxObj]; | |
} | |
let clone = map.get(obj); //clone obj (from map) | |
if (clone !== undefined) return [clone, indexedObjs]; //already processed | |
if (reverseMap.get(obj) !== undefined) return [obj, indexedObjs]; //obj is already my clone | |
let idxClone = null; // {"!objRefIdx!":<idx>} object: replacement for user object. | |
let cpy = (k, from = obj) => (clone[k] = (linearDeepCopy(from[k], map, reverseMap, indexedObjs)[0])); | |
if (Array.isArray(obj)) { | |
let props = Object.getOwnPropertyNames(obj); | |
[clone, idxClone] = register(obj, [...obj]); | |
for (let i = 0; i < obj.length; i++) { | |
let idx = props.indexOf(i.toString()); | |
props.slice(idx, idx + 1); //remove processed | |
cpy(i); | |
} | |
props.forEach(p => cpy(p)); //process unprocessed | |
//have to handle the indexes (<length) | |
//as well as user-added properties to array object. | |
} else if (typeof obj == 'object') { | |
[clone, idxClone] = register(obj, {}); | |
for (var k in obj) cpy(k); | |
} else { | |
clone = obj; //copy | |
return [clone, indexedObjs]; | |
} | |
return [idxClone, indexedObjs]; | |
} | |
function unlinearize(obj, niz, inplace = true, proccessedMap = undefined) { | |
if (inplace == false) niz = deepCopy(niz); //so we dont edit it. | |
if (niz.length == 0) return obj; // obj is primitive or doesnt reference other objects | |
if (typeof obj != 'object') return obj; //is primitive | |
if (proccessedMap === undefined) proccessedMap = new WeakMap(); | |
if (obj["!objRefIdx!"] !== undefined) obj = niz[obj["!objRefIdx!"]]; | |
if (inplace == false) obj = deepCopy(obj); //assumption: only initial call of unlinearize can have inplace=false | |
if (proccessedMap.get(obj)) return obj; //so we dont infinitely process circular references | |
proccessedMap.set(obj, true); | |
for (var k in obj) { | |
obj[k] = unlinearize(obj[k], niz, true, proccessedMap); | |
} | |
return obj; | |
} | |
/* | |
///////////////////////////////// | |
/// Deep copy test: | |
let o = {a:"Test"}; | |
let o2 = {a:o}; | |
o.z = o2; //circular | |
let orig = {x:o,y:o2,z:["str",o,o2]}; | |
let cpy = (deepCopy( | |
orig | |
)); | |
cpy.z[2].a.a = "NOT test anymore."; | |
console.log(orig); | |
console.log(cpy); | |
///////////////////////////////// | |
*/ | |
/* | |
///////////////////////////////// | |
/// Linearized deep copy test: | |
let o = {a:"Test"}; | |
let o2 = {a:o}; | |
o.z = o2; //circular | |
let cpy = (linearDeepCopy( | |
{x:o,y:o2,z:["str",o,o2]} | |
)); | |
let json = JSON.stringify(cpy); | |
cpy = JSON.parse(json); | |
// inplace=false example: | |
let cpy2 = unlinearize(...cpy,false); //inplace=false so we dont modify cpy[1] | |
cpy2.z[2].a.a = "NOT test anymore."; | |
console.log(json==JSON.stringify(cpy)); //true, unmodified due to inplace=false | |
console.log(cpy2); // correct circular changes. | |
// inplace=true would be like so: | |
cpy = unlinearize(...cpy); | |
cpy.z[2].a.a = "NOT test anymore2."; | |
console.log(cpy); | |
///////////////////////////////// | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment