Search code examples
javascriptnode.jsexpress

How do I get descriptor for JavaScript class constructor function?


Is it possible for me to get the descriptor for the actual constructor method of a JS class? I am writing my own little wrapper around Express to try and procedurally generate routes and stuff (and I am sure there are plenty of other libraries to do this, but it's a learning experience).

If I iterate through the function descriptors for a class prototype, I can convert the 'value' property to a string that represents a single method. If I call getString() on the descriptor for 'constructor' I get the entire class. I ONLY want the body of the constructor. I could add more parsing logic, but there has to be a way to do this.

Here is a simple controller:

const
    BaseController = require('../src/baseController');

/**
 * Handle requests for the home page
 */
class HomeController extends BaseController {
    /**
     * @param settings Controller settings
     * @param vault Injected vault client
     */
    constructor(settings, vault) {
        super(settings);
        this.vault = vault;
    }

    /**
     * Serve the landing page
     * @returns 
     */
    async get() {
        await this.renderAsync({ pageTitle: 'Pechanga Demo' });
    }
}

module.exports = HomeController;

Here is what currently comes back for 'get':

Object.getOwnPropertyDescriptor(controllerType.prototype, 'get').value.toString()

async get() {
   await this.renderAsync('index', { pageTitle: 'Pechanga Demo' });
}

However, getting the descriptor for 'constructor' returns the entire class body:

Object.getOwnPropertyDescriptor(controllerType.prototype, 'constructor').value.toString()

I really want is just:

    constructor(settings, vault) {
        super(settings);
        this.vault = vault;
    }

Is this possible without parsing the entire class body?


Solution

  • The issue here is that Javascript does not have the concept of classes, nor constructors. Yes, it might have a class and a constructor keyword in recent versions, but under the hood, it is still a prototype based langauge. Those keywords are just syntactic sugar - and that is what's causing the problem, because it makes you assume that .prototype.constructor had something to do with thwe constructor() syntax in classes. However, this is not the case.

    Let's take a look at this minimal example:

    class Bar {
        constructur (value) {
            this.property1 = value;
        }
    }
    
    class Foo extends Bar {
        constructor (value1, value2) {
            super(value1);
            this.property2 = value2;
        }
    }
    

    In traditional plain Javascript, you'd set this up like this:

    function Bar (value) {
        this.property1=value;
    }
    
    function Foo (value1, value2) {
        Bar.call(this,value1);
        this.property2 = value2;
    }
    Object.setPrototypeOf(Foo.prototype, Bar.prototype);
    

    which in turn is syntactic sugar for

    var Bar = function Bar (value) {
        this.property1=value;
    }
    var Foo = function Foo (value1, value2) {
        Bar.call(this,value1);
        this.property2 = value2;
    }
    Object.setPrototypeOf(Foo.prototype, Bar.prototype);
    

    Now, if we look at Foo, it's obviously a function - and we can clearly see that it is one as we assign one to the variable explicitely. As all functions, the toString() method of it will return it's source code (unless it's native).

    Again as all functions in Javascript, Foo has a property called .prototype which only exists to set up the proper [[Prototype]] for objects created with new Foo. This object then has a property called constructor which points at the function it was created for - which is Foo itself. So Foo.prototype.constructor.toString() is the same as Foo.toString() and will print the source code of Foo - just as you'd expect.

    You see clearly here: the .constructor property of a function's .prototype Property does not have any relation to the constructor() syntax in classes. It exists completely independently.

    Now, if you use the syntactic sugar that makes you think Javascript had classes, the toString() method of Foo.prototype.constructor will behave exactly as before: it will print the source code that defined Foo. Which is, in the "we used syntactic sugar"-case, class Foo extends Bar {constructor (value1, value2) {super(value1); this.property2 = value2;}}.

    So, what you are seeing is "working as expected".

    If you want the contents of constructor() alone, you need to either set it up the traditional way and not use the class-notation, or you need to parse the source code of your class and extract the relevant part. There is no function that will do this for you, and there is no object that corresponds to the contents of constructor(), as it does not actually exist within the compiled code.