Search code examples
javascriptecmascript-5prototype-chain

JS ECMA5: converting object to child class


Code example:

ClassA.js

var classB = require('./ClassB');

function ClassA() {
    this.ID = undefined;
    this.Type = undefined;
    ....
}

ClassA.prototype.init = function init(id){
    this.ID = id;

    this.get();

    if (this.Type === 'C' && Object.getPrototypeOf(this) === ClassA.prototype) {
        return new classB().init(this.ID);
    }
}

ClassB.js

function ClassB() {
    ClassA.call(this);
    this.additionalProp = undefined;
}

ClassB.prototype = Object.create(ClassA.prototype);
ClassB.prototype.constructor = ClassB;

I have implemented two classes ClassA and ClassB. ClassB is a child of CLassA and has some additional properties.

The prototype chain for ClassB is setup like this:

B.prototype = Object.create(A.prototype); B.prototype.constructor = B;

The information for the objects is retrieved from an API via an ID. At the time of the API call I do not know if the object needs to be an instance of ClassA or ClassB. So I always start with an object of ClassA to call the API. When the API returns a specific Type I want to convert my object from ClassA to more specific ClassB.

I tried to call the constructor of ClassB out of ClassA - this throws an error:

Uncaught TypeError: Object prototype may only be an Object or null: undefined

I don't think I should reference ClassB in ClassA at all, but it was worth a try...

Thank you for helping out a novice! :)


Solution

  • In a comment you've mentioned this error:

    Uncaught TypeError: Object prototype may only be an Object or null: undefined

    You get that because ClassA.js and ClassB.js have a circular relationship: each tries to import something the other exports. That can be fine in some cases, but here you have code in ClassB.js trying to use ClassA.prototype in top-level code, and ClassA.js importing ClassB from ClassB.js. You end up with a placeholder for the ClassA import and the Object.create(ClassA.prototype) call doesn't work.

    It's going to be much easier if you define both of them in the same file, thanks to function declaration hoisting.

    I'd also modify init so that it always returns the instance, since you need to return a new object sometimes but not other times. So have it always return the instance simplifies the calling code.

    Here's a minimal-changes example:

    function ClassA() {
        this.ID = undefined;
        this.Type = undefined;
        // ....
    }
    ClassA.prototype.get = function () {
        // *** Just for debugging, ID 2 is `ClassB`, any other is `ClassA`
        this.Type = this.ID === 2 ? "C" : "X";
    };
    
    ClassA.prototype.init = function init(id) {
        this.ID = id;
    
        this.get();
    
        if (this.Type === "C" && Object.getPrototypeOf(this) === ClassA.prototype) {
            return new ClassB().init(id); // *** Use `ClassB`, not `ClassA`
        }
    
        return this; // *** So the caller always gets an instance they can use
    };
    
    function ClassB() {
        ClassA.call(this);
        this.additionalProp = undefined;
    }
    
    ClassB.prototype = Object.create(ClassA.prototype);
    ClassB.prototype.constructor = ClassB;
    
    module.exports.ClassA = ClassA;
    module.exports.ClassB = ClassB;
    

    Then using it (just for example):

    var both = require("./both");
    var ClassA = both.ClassA;
    var ClassB = both.ClassB;
    
    var obj1 = new ClassA();
    obj1 = obj1.init(1);
    console.log(obj1 instanceof ClassA); // true
    console.log(obj1 instanceof ClassB); // false
    
    var obj2 = new ClassA();
    obj2 = obj2.init(2);
    console.log(obj2 instanceof ClassA); // true
    console.log(obj2 instanceof ClassB); // true
    

    That said, I think I'd refactor this. You've said that there's a separate init method because sometimes you want to use methods on the objects before you have an id. That makes me think ClassA (at least) is trying to do too much, both things that it can do when it doesn't know what it is (no id) and things it can do when it does. The instance returned by a constructor should be fully baked and ready to go. So probably better to split the parts of ClassA that don't need an id off into something else. That would also mean that ClassA didn't have to refer to ClassB, which isn't best practice.

    I think I'd probably also split get off to be separate from the classes, and have it return the appropriate instance.

    For example:

    ClassA.js:

    function ClassA(data) {
        this.ID = data.id;
        this.Type = data.type;
        // ....
    }
    
    // ...other `ClassA` methods...
    
    module.exports = ClassA;
    

    ClassB.js:

    var ClassA = require("./ClassA");
    
    function ClassB(data) {
        ClassA.call(this, data);
        this.additionalProp = data.additionalProp;
    }
    
    ClassB.prototype = Object.create(ClassA.prototype);
    ClassB.prototype.constructor = ClassB;
    
    // ...other `ClassB` methods...
    
    module.exports = ClassB;
    

    get.js (or whatever):

    var ClassA = require("./ClassA");
    var ClassB = require("./ClassB");
    
    function get(id) {
        var data = /*...get from API...*/;
        var cls = "additionalData" in data ? ClassB : ClassA;
        return new cls(data);
    }
    

    That provides a much better separation of concerns.