I wrote this code because I was often annoyed that I couldn't get the parent object. This class allows you to traverse the parent-child relationship between objects, in both directions. It also preserves prototypes.
For example:
let myTree = new Tree({a:{b:{k:2}}}); myTree.a.b; // { k: 2 } myTree.a === myTree.a.b.__parent__; // true It also enables you to add more properties:
myTree.c = {d:{l:5}}; // {d: {l: 5}} myTree.c.__parent__ === myTree; // true You can also move a branch:
myTree.a.b.__parent__ = myTree.c; // {d: {l: 5}} myTree.c.b.k; // 2 myTree.a.b; // undefined You can even move it to another tree:
let anotherTree = new Tree({}); myTree.a.l = 3; myTree.a.l; // 3 myTree.a.__parent__ = anotherTree; anotherTree.a; // {l: 3} myTree.a; // undefined anotherTree.a.__parent__ === anotherTree; // true Now, you may be wondering, how do you get a human readable and console-friendly version of this tree?
myTree.__raw__(); Note: only properties that are objects are able to give you the parent object.
My code:
const Tree = (function(){ const TARGET = Symbol('__target__'), HANDLER = Symbol('__handler__'), PROXY = Symbol('__proxy__'), { assign, defineProperty, entries, setPrototypeOf } = Object, convert=( obj )=>{ let res = new Branch(obj); entries(obj).forEach(([key, value], _) => { if( ({}).hasOwnProperty.call(obj, key) ) { if(typeof value === 'object') { res[key] = convert(value); defineProperty(res[key], '__parent__', { value: res[PROXY], configurable: false, writable: true }); } else { res[key] = value.constructor(value); } } }); return res; }, getKey = (obj, val) => { return entries(obj).filter(([key, value], _) => { return value[TARGET] === val; })[0][0]; }; let genHandler = (_target) => { return (function(){ let res = { set: (target, prop, value) => { if( ['__parent__'].includes(prop) ) { if( typeof value === 'object' && (value.__istree__ || value.__isbranch__) && value !== target.__parent__) { const key = getKey(target.__parent__, target); if(target.__parent__[key]) { delete target.__parent__[key]; } value[key] = target; return value; } else { throw TypeError('Cannot assign __parent__ to a non-tree value'); } } if(typeof value === 'object') { value = convert(value); defineProperty(value, '__parent__', { value: target[PROXY], configurable: false, writable: true }); } target[prop] = value; return value; }, setProxy: (val) => { res.proxy = val; }, get: (target, prop) => { if( prop === '__raw__' ) { return __raw__.bind(target); } if( prop === TARGET ) { return _target; } if( prop === HANDLER ) { return res; } if( prop === PROXY ) { return res.proxy; } return target[prop]; } }; return res; })() }; /** * Get the raw value of the tree, without all that proxy stuff. * @returns {Object} The raw object. Please not that objects will not be the same instances. * @memberof Tree# */ function __raw__() { let res = setPrototypeOf({}, this.__proto__); entries(this).forEach( ([key, value], _) => { if( {}.hasOwnProperty.call(this, key )) { if( typeof value === 'object') { res[key] = __raw__(value[TARGET]); } else { res[key] = value; } } }); return res; } /** * A class that enables navigation from child properties to parent. WIP - currently figuring out how to make new properties. * For all purposes this functions as intended, but it doesn't print well in the console. It even perserves prototypes. * @property {(Branch|*)} * Properties. * @property {(Tree|Branch)} __parent__ The parent element. This can be changed to move the object to another tree or branch. */ class Tree { /** * Constructs a new Tree instance. * @constructs Tree * @param {Object} obj The object to convert to a tree. * @throws {TypeError} You probably passed it a primitive. */ constructor(obj) { let conf = { __istree__: true }, res = new Proxy(setPrototypeOf(conf, obj.__proto__), genHandler(conf)); Object.defineProperty(res[HANDLER], 'proxy', { value: res, protected: true }); if( typeof obj !== 'object') { throw TypeError('Tree expects an object'); } else { for( let key in obj) { let value = obj[key]; if(typeof value === 'object') { res[key] = convert(value); defineProperty(res[key], '__parent__', { value: res[PROXY], configurable: false writable: true, }); } else { res[key] = value.constructor(value); }; }; }; return res; } } class Branch { constructor(obj) { let conf = { __isbranch__: true }, res = new Proxy(setPrototypeOf(conf, obj.__proto__), genHandler(conf)); Object.defineProperty(res[HANDLER], 'proxy', { value: res, protected: true }); return res; } } return Tree; })();