Search code examples
javascriptmixinsgetter

javascript mxins using `Object.assign`, evaluation earlier than expected


I have the fowlling mixins, with a setter and a getter:

const dataentity_compnts_mixins = {
    set within_context(val_obj) {
        switch (val_obj["dataentity_morphsize"]) {
            case 'compact':
                this.shadow_wrapper.setAttribute(
                    "within-context", "compact_dataentity");
                break;

            case 'expanded':
                this.shadow_wrapper.setAttribute(
                    "within-context", "expanded_dataentity");
                break;
        }
    },

    get within_context() {
        const host_dataentity = this.parent_dataentity ||
            this.closest('independent-data-entity') ||
            this.closest('dependent-data-entity');
        this.parent_dataentity = host_dataentity;

        const context_dict = {
            "dataentity_morphsize": host_dataentity.getAttribute("morph-size"),
            "dataentity_role": host_dataentity.getAttribute("entity-role")
        };
        return context_dict;
    }
};

then I use Object.assign to merge this into my custom element's prototype:

Object.assign(IndividualviewEditor.prototype, dataentity_compnts_mixins); [1]

I was expecting that the getter and the setter won't be evaluated until being called with this refering to the host object, which in my case is the IndividualviewEditor custom element. However, when run this code on my webpage, I got an error:

Uncaught TypeError: this.closest is not a function ...

I checked the callstack, which suggests that the getter is being called by the line [1].

I've done many searches on google and totally lost. This getter is evaluated when being merged into my prototype?? this is much earlier than I expected.


Solution

  • Object.assign copies all own enumerable properties to the left, meaning that getters are retrieved, and their returned value is copied, but the getter functionality itself is lost.

    const mixin = {
      get random() {
        console.log('I am mixin', this === mixin);
        return Math.random();
      }
    };
    
    const reference = {};
    
    Object.assign(reference, mixin);
    
    console.log("-------");

    With this code you'll read in console I am mixin true, because the random accessor is retrieved during the copy.

    The reference.random will indeed always point at the same number generated only during the Object.assign operation.

    To shallow copy properties you need to pass along descriptors, and the primitives to do so are Object.defineProperties and Object.getOwnPropertyDescriptors.

    Let's try again:

    const mixin = {
      get random() {
        console.log('I am mixin', this === mixin);
        return Math.random();
      }
    };
    
    const reference = {};
    
    Object.defineProperties(
      reference,
      Object.getOwnPropertyDescriptors(mixin)
    );
    
    console.log("-------");
    
    console.log(reference.random);
    reference.random = 4; //no setter - nothing happens
    console.log(reference.random);

    First thing to notice, is that you won't read anything in console, because the accessor is not, well, accessed.

    However, whenever you access reference.random you'll read that it's not the mixin context, and it will return a new random value each time.

    Whenever you want to define mixins for classes, or plain objects, Object.defineProperties is the way to go.