5
\$\begingroup\$

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;
})();
\$\endgroup\$
  • 1
    \$\begingroup\$ I have rolled back your last edit. Please don't change or add to the code in your question after you have received answers. See What should I do when someone answers my question? Thank you. \$\endgroup\$ – Phrancis Aug 4 '18 at 1:38
  • \$\begingroup\$ @Phrancis Thanks, I realized that an hour later, when I was going to sleep, and was going to roll it back - thanks for doing so! \$\endgroup\$ – FreezePhoenix Aug 4 '18 at 11:44
4
\$\begingroup\$

Looks like the examples you give don't work as intended.

> let myTree = new Tree({a:{b:{k:2}}});
undefined
> myTree.a.b;                         // { k: 2 }
Proxy [ { __isbranch__: true, k: 2 },
  { set: [Function: set],
    setProxy: [Function: setProxy],
    get: [Function: get] } ]
> myTree.a === myTree.a.b.__parent__; // true
true
> myTree.c = {d:{l:5}};               // {d: {l: 5}}
{ d: { l: 5 } }
> myTree.c.__parent__ === myTree;     // true
false
> myTree.a.b.__parent__ = myTree.c;   // {d: {l: 5}}
TypeError: Cannot set property '__parent__' of undefined
> myTree.c.b.k;                       // 2
TypeError: Cannot read property 'k' of undefined
> myTree.a.b;                         // undefined
undefined
> let anotherTree = new Tree({});
undefined
> myTree.a.l = 3;
3
> myTree.a.l;     // 3
3
> myTree.a.__parent__ = anotherTree;
Proxy [ { __istree__: true, a: Proxy [ [Object], [Object] ] },
  { set: [Function: set],
    setProxy: [Function: setProxy],
    get: [Function: get] } ]
> anotherTree.a;  // {l: 3}
Proxy [ { __isbranch__: true, l: 3 },
  { set: [Function: set],
    setProxy: [Function: setProxy],
    get: [Function: get] } ]
> myTree.a;       // undefined
undefined
> anotherTree.a.__parent__ === anotherTree; // true
false
> myTree.__raw__();
RangeError: Maximum call stack size exceeded

I'm thinking of one thing: what if the objects you operate were immutable, yet it was possible to operate on parent nodes of any of them? For example, in DOM, if you get an element and pass it as an argument to a function, the function has access to this element's properties, one of them is "parent". So technically you can go up from that node, until you reach document. I'm totally sure it might be useful, but querying object properties shouldn't affect its context.

So, taking the metaphor of DOM element, I thought of this:

class Element {
  constructor(object) {
    Object.entries(object).map(([property, value]) => {
      if (typeof value === 'object') {
        this[property] = new Element(value);
        this[property].__parent__ = this;
      } else {
        this[property] = value;
      }
    });

    this.__current__ = this;
  }

  current() {
    return this.__current__;
  }

  parent() {
    return this.__parent__;
  }

  get(property) {
    if (!this[property]) {
      throw new Error('No such property:', property);
    }

    return this[property];
  }
}

Take note that there are no Proxy or Symbol things for simplicity of the representation. In fact, I see __parent__ property being reassigned in your example, too, so I assume there's no need to wrap it into a Symbol.

Given that, we can make an instance of Element object:

let e = new Element({ a: 1, b: { c: 2, d: { e: 3, f: { g: 4, h: { i: 5, j: {} } } } } });

so that it has the properties that we want to have:

> e.current() === e
true
> e.get('b').parent() === e
true
> e.get('b').get('d').parent() === e.get('b')
true
> e.get('b').current() === e.get('b')
true
> e.b === e.get('b')
true
> e.b.d === e.get('b').get('d').get('f').get('h').parent().parent()
true

We can safely pass a certain property within the object to a function and get the parent of that property. So given a function

function getParent(element) {
  return element.parent();
}

we get

> getParent(e.b.d) === e.b
true

But the thing I was talking about was circularity. In case of this simplistic structure, we can make it circular easily:

e.e = e;

so that

> e.e.e === e.e
true
> e.e.e.e.e.e.e.e === e.e.e.e
true

which supports your use case, too: we can assign another instance of this object to some property and enjoy the traversable structure:

> e.b.another = new Element({ abc: 'xyz' })
Element { abc: 'xyz', __current__: [Circular] }
> e.b
Element {
  c: 2,
  d:
   Element {
     e: 3,
     f:
      Element {
        g: 4,
        h: [Element],
        __current__: [Circular],
        __parent__: [Circular] },
     __current__: [Circular],
     __parent__: [Circular] },
  __current__: [Circular],
  __parent__:
   Element { a: 1, b: [Circular], __current__: [Circular], e: [Circular] },
  another: Element { abc: 'xyz', __current__: [Circular] } }
> e.b.another.e = e
Element {
  a: 1,
  b:
   Element {
     c: 2,
     d:
      Element {
        e: 3,
        f: [Element],
        __current__: [Circular],
        __parent__: [Circular] },
     __current__: [Circular],
     __parent__: [Circular],
     another:
      Element { abc: 'xyz', __current__: [Circular], e: [Circular] } },
  __current__: [Circular],
  e: [Circular] }
> e.b.another.e.b.another === e.b.another
true

as well as we can assign a certain position in our tree to another variable and check if they are the same:

> let copyOfE = e.b.another.e; copyOfE === e
true

However, you might have noticed that we operate on pointers here, so any assignment to a property is destructive:

> copyOfE.b = 2
2
> e.b
2

I'm not sure if you're looking after this behavior, too, but if we go full on OOP, that's what we get by default.

Now, the raw thing. If you call this method toJSON, it will be guaranteed to be called by JSON.stringify. So we add this method to our class:

class Element {
  constructor(object) {
    Object.entries(object).map(([property, value]) => {
      if (typeof value === 'object') {
        this[property] = new Element(value);
        this[property].__parent__ = this;
      } else {
        this[property] = value;
      }
    });

    this.__current__ = this;
  }

  current() {
    return this.__current__;
  }

  parent() {
    return this.__parent__;
  }

  get(property) {
    if (!this[property]) {
      throw new Error('No such property:', property);
    }

    return this[property];
  }

  toJSON() {
    const reducer = element => {
      let output = {};

      for (let property in element) {
        if (property.slice(0, 2) === property.slice(-2) && property.slice(-2) === '__') {
          continue;
        } else if (typeof element[property] !== 'object') {
          output[property] = element[property];
        } else {
          output[property] = reducer(element[property]);
        }
      }

      return output;
    };    
    return reducer(this);
  }
}

and enjoy output without circular references:

> JSON.stringify(new Element({ a: 1, b: { c: 2, d: { e: 3, f: { g: 4, h: { i: 5, j: {} } } } } }))
'{"a":1,"b":{"c":2,"d":{"e":3,"f":{"g":4,"h":{"i":5,"j":{}}}}}}'

However, it's still prone to stack overflows:

> e.e = e
Element {
  a: 1,
  b:
   Element {
     c: 2,
     d:
      Element {
        e: 3,
        f: [Element],
        __current__: [Circular],
        __parent__: [Circular] },
     __current__: [Circular],
     __parent__: [Circular] },
  __current__: [Circular],
  e: [Circular] }
> JSON.stringify(e)
RangeError: Maximum call stack size exceeded

There's a good Q&A on StackOverflow about this topic, check it out.

All in all, I think you are right about being defensive and using proxy objects, but still the code could be simplified a little bit, split into separate small utility functions, organized in a way to be easy to read from top to bottom, and such.

If the structure is circular, like a tree, you can treat every node as a tree or as a branch, unless it really provides value to the user to distinguish between the two.

And if after a couple months you find yourself struggling to understand what your code does after reading it once or twice (just like I did), it puts you at potential risk of having real hard time maintaining the code. So hopefully I provided an idea or two on how it could be optimized for maintainability. Cheers!

\$\endgroup\$
  • \$\begingroup\$ I used proxy objects so that I could detect new properties being added, and make some dynamic properties that didn't show up on the object. \$\endgroup\$ – FreezePhoenix Aug 3 '18 at 22:46
  • \$\begingroup\$ "I'm totally sure it might be useful, but querying object properties shouldn't affect its context." - did you intend to type "I'm not totally sure..."? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Sep 26 '18 at 17:37
  • \$\begingroup\$ I’m totally sure I was totally sure back in time. :) \$\endgroup\$ – rishat Sep 26 '18 at 20:52
  • \$\begingroup\$ Querying object properties in my code doesn't change it's context... \$\endgroup\$ – FreezePhoenix Sep 27 '18 at 12:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.