Search code examples
ecmascript-6constructorfieldes6-class

Run a 'constructor' or function, after subclass fields initialized, in a sane way?


I'd like to use ES6 public class fields:

class Superclass {
    constructor() {
        // would like to write modular code that applies to all
        // subclasses here, or similarly somewhere in Superclass
        
        this.example++;  // does NOT WORK (not intialized)
        //e.g. doStuffWith(this.fieldTemplates)
    }
}

class Subclass extends Superclass {
    example = 0
    static fieldTemplates = [
        Foo, 
        function() {this.example++}, 
        etc
    ]
}

Problem:

ES6 public fields are NOT initialized before the constructors, only before the current constructor. For example, when calling super(), any child field will not yet have been defined, like this.example will not yet exist. Static fields will have already been defined. So for example if one were to execute the code function(){this.example++} with .bind as appropriate, called from the superclass constructor, it would fail.

Workaround:

One workaround would be to put all initialization logic after all ES6 public classes have been properly initialized. For example:

class Subclass extends Superclass {
    example = 0
    lateConstructor = (function(){
        this.example++; // works fine
    }).bind(this)()
}

What's the solution?

However, this would involve rewriting every single class. I would like something like this by just defining it in the Superclass.constructor, something magic like Object.defineProperty(this, 'lateConstructor', {some magic}) (Object.defineProperty is allegedly internally how es6 static fields are defined, but I see no such explanation how to achieve this programatically in say the mozilla docs; after using Object.getOwnPropertyDescriptor to inspect my above immediately-.binded-and-evaluated cludge I'm inclined to believe there is no way to define a property descriptor as a thunk; the definition is probably executed after returning from super(), that is probably immediately evaluated and assigned to the class like let exampleValue = eval(...); Object.defineProperty(..{value:exampleValue})). Alternatively I could do something horrible like do setTimeout(this.lateConstructor,0) in the Superclass.constructor but that would break many things and not compose well.

I could perhaps try to just use a hierarchy of Objects everywhere instead, but is there some way to implement some global logic for all subclasses in the parent class? Besides making everything lazy with getters? Thanks for any insight.

References:

Run additional action after constructor -- (problems: this requires wrapping all subclasses)


Solution

  • Just thought of a workaround (that is hierarchically composable). To answer my own question, in a somewhat unfulfilling way (people should feel free to post better solutions):

    // The following illustrates a way to ensure all public class fields have been defined and initialized
    // prior to running 'constructor' code. This is achieved by never calling new directly, but instead just
    // running Someclass.make(...). All constructor code is instead written in an init(...) function.
    
    class Superclass {
        init(opts) {  // 'constructor'
            this.toRun();  // custom constructor logic example
        }
        
        static make() {  // the magic that makes everything work
            var R = new this();
            R.init(...arguments);
            return R;
        }
    }
        
    class Subclass extends Superclass {
        subclassValue = 0  // custom public class field example
    
        init(toAdd, opts) {  // 'constructor'
            // custom constructor logic example
            this.subclassValue += toAdd;  // may use THIS before super.init
            
            super.init(opts);
    
            // may do stuff afterwards
        }
    
        toRun() {          // custom public class method example
            console.log('.subclassValue = ', this.subclassValue);
        }
    }
    

    Demo:

    > var obj = Subclass.make(1, {});
    .subclassValue =  1
    
    > console.log(obj);
    Subclass {
        subclassValue: 1
        __proto__: Superclass
    }