1
\$\begingroup\$

This code builds on the code in the previous iteration. All of the following tests should work:

let myTree = Tree.from({a:{b:{k:2}}});
myTree.a.b;                         // { k: 2 }
myTree.a === myTree.a.b.__parent__; // true
// adding properties
myTree.c = {d:{l:5}};               // {d: {l: 5}}
myTree.c.__parent__ === myTree;     // true
myTree.a.b.__parent__ = myTree.c;   // {d: {l: 5}}
myTree.c.b.k;                       // 2
myTree.a.b;                         // undefined
// moving branches to other trees.
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

It also introduces the Fragment class, which behaves both like a tree and a branch in the fact that it can be assigned to both and have child properties.

Upon an object or primitive being assigned, it is immediately converted to a branch or a leaf, respectively.

All leaves are dynamically inherited.

Code:

const [Tree, Fragment] = ( function(){
    const LeafCache = Object.create( null ),
     genLeaf = ( CLASS ) => {
       return LeafCache[CLASS.name] || LeafCache[CLASS.name] = eval( `class Leaf extends ${CLASS.name} {
    constructor(val) {
        super(val);
    }
}; Leaf.prototype.__isleaf__ = true; Leaf` );
        },
     TARGET = Symbol( '__target__' ),
        HANDLER = Symbol( '__handler__' ),
        PROXY = Symbol( '__proxy__' ),
        ISTREE = Symbol( '__istree__' ),
        { assign, defineProperty, entries, setPrototypeOf } = Object,
        { hasOwnProperty } = {},
        convert = ( obj ) => {
          let res = obj instanceof Branch ? obj : new Branch( obj );
          for( const key in obj ) {
            const value = obj[key];
            if( hasOwnProperty.call( obj, key ) ) {
              if( '__isfragment__' !== key ) {
                if( typeof value === 'object' ) {
                    res[key] = convert( value );
                    Object.defineProperty( res[key], '__parent__', {
                      value: res[PROXY],
                      configurable: false,
                      protected: false
                    } );
                } else {
                  let val;
                  res[key] = new Proxy( val = new ( genLeaf( value.constructor ) )( value ), genHandler( val ) );
                }
              }
            }
          }
          return res;
        },
        getKey = ( obj, val ) => {
          for( const key in obj ) {
            const value = obj[key];
            if( value[TARGET] === val ) {
              return key;
            }
          }
        };
    let genHandler = ( _target ) => {
        return ( function(){
            let res;
            const _raw = __raw__.bind( _target ),
                _keys = {
                    '__raw__': _raw,
                    [ISTREE]: true,
                    [TARGET]: _target,
                    get [PROXY]() {
                        return res.proxy;
                    },
                    get [HANDLER]() {
                        return res;
                    }
                };
            res = {
                set: ( target, prop, value ) => {
                    if( prop === '__parent__' ) {
                        if( _keys[PROXY] instanceof Fragment ) {
                            throw TypeError( 'Cannot set __parent__ on fragments.' );
                        } else if( typeof value === 'object' && value[ISTREE] ) {
                            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.constructor.name !== 'Leaf' ) {
                        value = convert( value );
                        if( value[PROXY] instanceof Tree ) {
                            throw TypeError( 'Cannot have a tree as a child of another tree.' );
                        }
                        value = convert( value );
                        defineProperty( value, '__parent__', {
                            value: _keys[PROXY],
                            configurable: false,
                            writable: true
                        } );
                    } else if ( typeof value !== 'object' ) {
                        let val;
                        value = new Proxy( val = new ( genLeaf( value.constructor ) )( value ), genHandler( val ) );
                    }
                    target[prop] = value;
                    return value;
                },
                get: ( target, prop ) => {
                   if( prop === 'toJSON' ) {
                     return _raw;
                   }
                   if( [HANDLER, PROXY, '__raw__', ISTREE, TARGET].includes( prop ) ) {
                     return _keys[prop];
                   }
                    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__ );
        for( const key in this ) {
            if( key.slice( 0, 2 ) === key.slice( -2 ) && key.slice( -2 ) === '__' ) {
            continue;
        } else {
                const value = this[key];
                if( hasOwnProperty.call( this, key ) ) {
                    res[key] = typeof value === 'object' ? __raw__.call( value[TARGET] ) : 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|Leaf)} * Properties.
     */
    class Tree {
        /**
         * Constructs a new Tree instance.
         * @constructs Tree
         */
        constructor() {
            return Tree.from( {} );
        }
        /**
         * Attempt to create a tree from an existing object.
         * @param {Object} obj The object to convert to a tree.
         * @throws {TypeError} You probably passed it a primitive.
         * @returns {Tree} The resulting tree.
         */
        static from( obj ) {
            const self = {},
                res = new Proxy( setPrototypeOf( self, obj.__proto__ ), genHandler( self ) );
            defineProperty( res[HANDLER], 'proxy', {
                value: res,
                configurable: false,
                protected: true
            } );
            if( typeof obj !== 'object' ) {
                throw TypeError( 'Tree expects an object' );
            } else {
                for( const key in obj ) {
                    const value = obj[key];
                    let val;
                    res[key] = typeof value === 'object' ? convert( value ) : new Proxy( val = new ( genLeaf( value.constructor ) )( value ), genHandler( val ) );
                    console.log( res[key][TARGET] );
                }
            }
            defineProperty( res, '__istree__', {
                value: true,
                configurable: false,
                protected: true
            } );
            return res;
        }
        static [Symbol.hasInstance]( obj ) {
          return obj[ISTREE] && obj.__istree__ || false;
        }
    }
    /**
     * A class that behaves similar to a tree and similar to a branch. It can be added to a tree like a branch.
     * @class
     */
    class Fragment {
        /**
         * Construct a new fragment.
         * @constructs Fragment
         */
        constructor() {
            return Fragment.from( {} );
        }
        /**
         * Attempt to make a fragment from an existing object.
         * @param {Object} obj The object to use.
         * @returns {Fragment} The resulting fragment.
         */
        static from( obj ) {
            const self = {},
                res = new Proxy( setPrototypeOf( self, obj.__proto__ ), genHandler( self ) );
            defineProperty( res[HANDLER], 'proxy', {
             value: res,
             configurable: false,
             protected: true
            } );
            if( typeof obj !== 'object' ) {
                throw TypeError( 'Tree expects an object' );
            } else {
                  for( let key in obj ) {
                    let value = obj[key];
                    res[key] = typeof value === 'object' ? convert( value ) : value;
                  }
            }
            defineProperty( res, '__isfragment__', {
                value: true,
                configurable: false,
                protected: true
            } );
            return res;
        }
        static [Symbol.hasInstance]( obj ) {
          return obj[ISTREE] && obj.__isfragment__ || false;
        }
    }
    class Branch {
        constructor( obj ) {
          let self = {},
                res = new Proxy( setPrototypeOf( self, obj.__proto__ ), genHandler( self ) );
            defineProperty( res[HANDLER], 'proxy', {
                value: res,
                configurable: false,
                protected: true
            } );
            defineProperty( res, '__isbranch__', {
                value: true,
                configurable: false,
                protected: true
            } );
            return res;
        }
        static [Symbol.hasInstance]( obj ) {
          return obj[ISTREE] && obj.__isbranch__ || false;
        }
    }
    return [Tree, Fragment];
} )();
/**
 * A class that shows that an item is a terminal node. Parent properties cannot be accessed by this node.
 * @alias Brance
 * @class Leaf
 * @extends Primitive
 */
/**
 * A class that simply shows that it is an inner object of a Tree.
 * @alias Branch
 * @class Branch
 * @property {(Tree|Branch)} __parent__ The parent element. This can be changed to move the object to another tree or branch.
 */

This is an iterative review. Previous iteration: Traversing the parent-child relationship between objects

\$\endgroup\$
1
\$\begingroup\$

Errors

I tried this both in jsFiddle (with and without JS 1.7 and Babel) as well as NodeJS but saw a few errors. The first one was:

  return LeafCache[CLASS.name] || LeafCache[CLASS.name] = eval( `class Leaf extends ${CLASS.name} {
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

ReferenceError: Invalid left-hand side in assignment

So then I tried wrapping the second conditional in parentheses and that appeared to get me past that error:

return LeafCache[CLASS.name] || (LeafCache[CLASS.name] = eval( `class Leaf extends ${CLASS.name} {
    constructor(val) {
        super(val);
    }
}; Leaf.prototype.__isleaf__ = true; Leaf` ));

But then I see another error:

               Object.defineProperty( res[key], '__parent__', {
                       ^

TypeError: Cannot redefine property: __parent__

Other feedback

Other than those errors I don't see much I would change - maybe a few places where variables can be declared using const instead of let - e.g. genHandler, res within the function __raw__, so as to avoid unintentional re-assignment...

Some of this code appears to be identical to the changes added in revision 7 to your previous post but with the Leaf class added. Was that in response to the suggestion at the end of rm-'s answer?

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.1

1https://codereview.stackexchange.com/a/200942/120114

\$\endgroup\$
  • \$\begingroup\$ The Leaf class is mostly in response to the answer, yes, and also to show dynamic inheritance. Thanks. I think one more post should do this project. It'll be ready in about a week. or 2. \$\endgroup\$ – FreezePhoenix Sep 27 '18 at 12:03

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.