(function(){
"use strict";
var ignore = [Boolean, Date, Number, RegExp, String];
function primitive(item){
if (typeof item === 'object'){
if (item === null) { return true; }
for (var i=0; i<ignore.length; i++){
if (item instanceof ignore[i]) { return true; }
}
return false;
} else {
return true;
}
}
function infant(value){
return Array.isArray(value) ? [] : {};
}
JSON.decycleIntoForest = function decycleIntoForest(object, replacer) {
if (typeof replacer !== 'function'){
replacer = function(x){ return x; }
}
object = replacer(object);
if (primitive(object)) return object;
var objects = [object];
var forest = [infant(object)];
var bucket = new WeakMap(); // bucket = inverse of objects
bucket.set(object, 0); // i.e., map object to index in array
function addToBucket(obj){
var result = objects.length;
objects.push(obj);
bucket.set(obj, result);
return result;
}
function isInBucket(obj){
return bucket.has(obj);
// objects[bucket.get(obj)] === obj, iff true is returned
}
function processNode(source, target){
Object.keys(source).forEach(function(key){
var value = replacer(source[key]);
if (primitive(value)){
target[key] = {value: value};
} else {
var ptr;
if (isInBucket(value)){
ptr = bucket.get(value);
} else {
ptr = addToBucket(value);
var newTree = infant(value);
forest.push(newTree);
processNode(value, newTree);
}
target[key] = {pointer: ptr};
}
});
}
processNode(object, forest[0]);
return forest;
};
})();
the = document.getElementById('the');
function consoleLog(toBeLogged){
the.textContent = the.textContent + '\n' + toBeLogged;
}
function show(root){
var cycleFree = JSON.decycleIntoForest(root);
var shown = cycleFree.map(function(tree, idx){ return false; });
var indentIncrement = 4;
function showItem(nodeSlot, indent, label){
leadingSpaces = ' '.repeat(indent);
leadingSpacesPlus = ' '.repeat(indent + indentIncrement);
if (shown[nodeSlot]){
consoleLog(leadingSpaces + label + ' ... see above (object #' + nodeSlot + ')');
} else {
consoleLog(leadingSpaces + label + ' object#' + nodeSlot);
var tree = cycleFree[nodeSlot];
shown[nodeSlot] = true;
Object.keys(tree).forEach(function(key){
var entry = tree[key];
if ('value' in entry){
consoleLog(leadingSpacesPlus + key + ": " + entry.value);
} else {
if ('pointer' in entry){
showItem(entry.pointer, indent+indentIncrement, key);
}
}
});
}
}
showItem(0, 0, 'root');
}
cities4d = {
Europe:{
north:[
{name:"Stockholm", population:1000000, temp:6},
{name:"Helsinki", population:650000, temp:7.6}
],
south:[
{name:"Madrid", population:3200000, temp:15},
{name:"Rome", population:4300000, temp:15}
]
},
America:{
north:[
{name:"San Francisco", population:900000, temp:14},
{name:"Quebec", population:530000, temp:4}
],
south:[
{name:"Rio de Janeiro", population:7500000, temp:24},
{name:"Santiago", population:6300000, temp:14}
]
},
Asia:{
north:[
{name:"Moscow", population:13200000, temp:6}
]
}
};
cities4d.Europe.north[0].alsoStartsWithS = cities4d.America.north[0];
cities4d.Europe.north[0].smaller = cities4d.Europe.north[1];
cities4d.Europe.south[1].sameLanguage = cities4d.America.south[1];
cities4d.Asia.ptrToRoot = cities4d;
show(cities4d)
<pre id="the"></pre>