Search code examples
javascriptclassecmascript-6

Why properties initializing don't propagate to child class in ECMASCRIPT?


I'm having some trouble wrapping my head around inheritance in ES6 classes. Searching here, most of the answers are 10+ years old so I don't think they apply still. Let's start with the sample code:

class BaseClass {
    baseUnitializedProp;
    baseInitializedProp = 'abc';
    constructor(properties) {
        Object.assign(this, properties);
        console.log('--- BaseClass Constructor ---');
        console.log(this);
        return this;
    }    
}
    
class ClassWithProp extends BaseClass {
    childUnitializedProp;
    childInitializedProp = 1;
}
    
class ClassWithOwnAssign extends ClassWithProp {
    constructor(properties) {
        const instance = super();
        Object.assign(this, properties);
        return instance;
    }    
}
    
const initedObj = new ClassWithProp({
    childInitializedProp: 20, 
    childUnitializedProp: 10, 
    baseUnitializedProp: 15
});
    
console.log('--- Post initialization ---');
console.log(initedObj);
    
const assignedObj = new ClassWithOwnAssign({
    childInitializedProp: 20, 
    childUnitializedProp: 10, 
    baseUnitializedProp: 15
});
console.log('--- Post initialization ---');
console.log(assignedObj);

My question is, why don't the child properties assignment propagate? From the console.log inside the constructor you see that the properties were assigned, but outside the same properties end up with the declared initial value (or undefined).


Solution

  • The order of execution

    • Child constructor is called if any, but you are forced to call the base's class constructor with super() if any to work with this
    • Base class declared props are initialized (assigned to this)
    • Base class constructor is called if any
    • Child class declared props are initialized (assigned to this)
    • Child class constructor is executed after super() (if any)

    In your first case the properties that you pass are gone into the base class constructor and assigned to this, but then overridden by the declared properties in the child class.

    In your second case you don't pass any properties to the base constructor so it shows only declared initialized base class properties, than you assign all properties in the child constructor.

    In this design pattern you can control the assignment precisely: in each constructor you can assign only props that belongs to the constructor's class. Any non declared in the classes props are skipped. I liked this one (made it right now):

    class BaseClass {
        #props = new Set;
        baseUnitializedProp;
        baseInitializedProp = 'abc';
        constructor(properties) {
            console.log('--- BaseClass Init ---');
            console.log(this);
            this.init(properties);
            console.log('--- BaseClass Constructor ---');
            console.log(this);
        }
        init(properties){
          for(const k of Object.getOwnPropertyNames(this)){
            if(k in properties && !this.#props.has(k)){
              this[k] = properties[k];
              this.#props.add(k);
            }
          }
        }
    }
        
    class ClassWithProp extends BaseClass {
        childUnitializedProp;
        childInitializedProp = 1;
        constructor(properties){
          console.log('--- ClassWithProp Constructor called ---');
          super(properties);
          console.log('--- ClassWithProp Init ---');
          console.log(this);      
          this.init(properties);
          console.log('--- ClassWithProp Constructor after super() ---');
          console.log(this);
        }
    }
        
    const initedObj = new ClassWithProp({
        childInitializedProp: 20, 
        childUnitializedProp: 10, 
        baseUnitializedProp: 15,
        iAmDummyPleaseSkipMe: 'skipped'
    });
        
    console.log('--- Post initialization ---');
    console.log(initedObj);
    .as-console-wrapper{max-height: 100% !important}