JS Tip of the Day: Overriding Constructor Returns

Overriding Constructor Returns
Level: Advanced

Constructors are used to make new object instances. When you see the new keyword followed by an object type (represented by a constructor) you’ll know that an instance of that type is being created.

let noodleInstance = new Noodle();

But in actuality, this may not necessarily be the case. Objects returned from constructors called with new may return something else entirely. While the default, implicit behavior of constructors is to return the object instantiated by that constructor function, it is possible for constructors to override that return with a different object value by specifying an explicit return.

class Noodle {
    constructor () {
        return new Rice();
    }
}

let riceInstance = new Noodle(); // (could it be a rice noodle?)

You should do this sparingly, however, as it violates the expectations set forth by the constructor. That is, if you see new something, you should expect to get an instance of that something in return. Nevertheless, there are cases where it can become useful. One example is with object pooling, the process by which instances that are no longer needed are kept in memory and reused for future new instances rather than having to recreate those new instances again from scratch.

class Noodle {
    constructor () {
        if (Noodle.pool.length) {
            return Noodle.pool.pop(); // use pooled instance
        }
        // otherwise implicitly returns new instance
    }

    destroy () {
        Noodle.pool.push(this); // in pool for future new instances
    }
}
Noodle.pool = []; // pool is static property on Noodle class

let newNoodle = new Noodle(); // new instance
newNoodle.destroy(); // done using newNoodle, adds to pool

let wetNoodle = new Noodle(); // gets instance from pool
console.log(wetNoodle === newNoodle); // true

Here, pooling is used to override the return value of the constructor with values from the pool if available. And in doing so, it’s continuing to return Noodle instances which is what someone calling new would expect to get from the constructor.

Note that in returning an explicit object from a constructor like this, you’re not stopping the original object from being created. Even when a pooled instance of Noodle is returned, a new Noodle is still getting instantiated and assigned to this as part of that constructor getting called (though we’ll look at an exception to this later on). If you wish this not to happen, you may want to consider using a factory function instead.

class Noodle {
    static create () {
        if (Noodle.pool.length) {
            return Noodle.pool.pop(); // use pooled instance
        }
        return new Noodle(); // use new instance
    }
    // ...
}
// ...
let newNoodle = Noodle.create();
// ...

More info: