javascriptclassdata-structuresgetter-setterdeep-copy

How to pass an object to a constructor where the former becomes part of the instance's data and its getters can reach class specific props and methods


Is it possible to "import" or "pass" a pre-defined object into a class that has access to methods on that class? Importantly, for performance reasons, I need to ensure that I am not re-creating or re-defining the "massive_entity_obj" in the example

class Distinct_Class {
   entities = null;

   constructor(massive_entity_obj) {
      this.entities = massive_entity_obj;
   }

   unique_func_1() {
      const some_value = "calcs based on unique data of Distinct_Class";
      return some_value;
   }
}

// defined in a separate module
massive_entity_obj = {
   property_1: {
      sub_property_a: "something",
      get sub_property_b() {
         // call a function in Distinct_Class in the context of Distinct_Class
         return this.unique_func_1(); // <-- this does not work
      },
   },
};

const distinct_1 = new Distinct_Class(massive_entity_obj)
const distinct_2 = new Distinct_Class(massive_entity_obj)
let sub_prop = distinct_1.entities.property_1.sub_property_b
let other_sub_prop = distinct_2.entities.property_1.sub_property_b

Solution

  • What the OP is looking for can be solved entirely generically.

    The only assumption one has to make about the to be injected "massive data blob" is that one is going to deal with it as a data structure, where each of this structure's (own) entry (key-value pair), regardless of nested or not, gets treated through its property descriptor.

    This setting allows a combined cloning and delegation/binding strategy for getters and setters which are functions, hence their context (the thisArg) can be changed via bind.

    Implementing the approach as recursive function, one easily achieves a deep and real clone of any provided data-blob (due to the cloning of property descriptors), whilst also being able of rebinding any getter or setter to the context of the involved class-instance at construction/instantiation time.

    // e.g. defined in separate modules
    
    // ----- ----- ----- ----- -----
    class DistinctClass {
    
      #state;
      entities = null;
    
      constructor(dataBlob, protectedState) {
        this.#state = structuredClone(protectedState);
    
        this.entities =
          cloneStructureAndRebindAnyGetAndSet(this, dataBlob);
    
        console.log({
          'injected blob': dataBlob,
          'this.entities': this.entities,
        });
      }
      uniqueDataProcessingMethod() {
        console.log('calcs based on unique instance data/state.');
    
        return structuredClone(this.#state);
      }
    }
    // ----- ----- ----- ----- -----
    
    // ----- ----- ----- ----- -----
    const massiveDataBlob_1 = {
       property_1: {
          subProperty_A: "sub property A",
          get subProperty_B() {
             // - invocation of a `DistinctClass` instance specific
             //   method within the context of this very instance.
             return this?.uniqueDataProcessingMethod?.() ?? this;
          },
       },
    };
    // ----- ----- ----- ----- -----
    
    // ----- ----- ----- ----- -----
    const massiveDataBlob_2 = {
       property_2: {
          subProperty_C: "sub property C",
          get subProperty_D() {
             // - invocation of a `DistinctClass` instance specific
             //   method within the context of this very instance.
             return this?.uniqueDataProcessingMethod?.() ?? this;
          },
       },
    };
    // ----- ----- ----- ----- -----
    
    const distinct_A =
      new DistinctClass(massiveDataBlob_1, { foo: 'FOO', bar: 'BAR' });
    
    const distinct_B =
      new DistinctClass(massiveDataBlob_2, { baz: 'BAZ', bizz: 'BIZZ' });
    
    const distinctResult_A =
      distinct_A.entities.property_1.subProperty_B;
    
    const distinctResult_B =
      distinct_B.entities.property_2.subProperty_D;
    
    console.log({
      distinctResult_A,
      distinctResult_B,
    });
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    function isFunction(value) {
      return (
        typeof value === 'function' &&
        typeof value.call === 'function' &&
        typeof value.apply === 'function'
      );
    }
    
    function cloneStructureAndRebindAnyGetAndSet(context, data, result = {}) {
      if (Array.isArray(data)) {
    
        result = Array.map(item =>
          cloneStructureAndRebindAnyGetAndSet(context, item)
        );
    
      } else if (!!data && typeof data === 'object') {
    
        result = Object
    
          .keys(data)
          .reduce((target, key) => {
    
            const descriptor = Reflect.getOwnPropertyDescriptor(data, key);
            let { set, get, value } = descriptor;
      
            if (isFunction(set)) {
              set = set.bind(context);
            }
            if (isFunction(get)) {
              get = get.bind(context);
            }
            
            if (Object.hasOwn(descriptor, 'value')) {
    
              if (!!value && typeof value === 'object') {
    
                value = cloneStructureAndRebindAnyGetAndSet(context, value);
              }
              Reflect.defineProperty(target, key, { ...descriptor, value });
    
            } else {
    
              Reflect.defineProperty(target, key, {
                ...descriptor,
                ...(set ? { set } : {}),
                ...(get ? { get } : {}),
              });
            }
            return target;
    
          }, result);
    
      } else {
    
        result = data;
      }
      return result;
    }
    </script>