JS ECMA5: converting object to child class

91 views Asked by At

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! :)

2

There are 2 answers

5
T.J. Crowder On BEST ANSWER

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.

1
Bergi On

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 one class, then when the API returns a specific type I want to convert my object.

Just don't do that. Construct your object after the API returns, and construct it with the correct type in the first place.

I always start with an object of the class to call the API

This is the point where your design has gone wrong. The class should not be responsible for calling the API, it should be responsible only for representing the data and having methods act on the data. Don't start with an empty object that can somehow hydrate itself, have the constructor completely initialise the object from the data passed as parameters.

Put the API call in a separate function, outside of the class. Create a separate class to represent the API endpoint maybe. If absolutely necessary, make it a static method of your base class, it still can create new ClassA or new ClassB instances then.