JS Tip of the Day: super() Defines this

super() Defines this
Version: ES2015
Level: Advanced

In function-based constructors, the instance being constructed is created as soon as the constructor is invoked and is immediately accessible through the this keyword. If extending another class/constructor, running the superclass constructor is handled by explicitly calling the superclass constructor in the context of that new instance.

function Button () {
    this.enabled = true;
}

function ToggleButton () {
    // this instance created
    this.toggled = false;
    Button.call(this); // superclass constructor
}

// extends
ToggleButton.prototype = Object.create(Button.prototype);
ToggleButton.prototype.constructor = ToggleButton;

let toggleButton = new ToggleButton();
console.log(toggleButton.enabled); // true (from superclass)

Using the class syntax, however, when one class extends another, super() is used to run the superclass’s constructor. This call is required and must be called prior to accessing this within the subclass’s constructor. The reason is, this - the instance being constructed - is created by the superclass through the call to super(). Unlike function constructors, this simply doesn’t exist until the superclass constructor is called.

class Button {
    constructor () {
        this.enabled = true;
    }
}

class ToggleButton extends Button {
    constructor () {
        // this instance does not yet exist
        // this.toggled = false; // ReferenceError: must call super
        super(); // Calls Button constructor, creates instance
        // this instance now exists
        this.toggled = false; // Ok
    }
}

let toggleButton = new ToggleButton();
console.log(toggleButton.enabled); // true (from superclass)

This illustrates a key difference in behavior between function constructors and class constructors: function constructors construct from the bottom up, whereas class constructors construct from the top down.

  • function: subclass creates, passes instance to superclasses
  • class: superclass creates, passes instance to subclasses

The effects of this can be more clearly seen when the superclass returns another object explicitly:

class Button {
    constructor () {
        return [1, 2, 3];
    }
}

class ToggleButton extends Button {
    constructor () {
        super();
        console.log(this); // [1, 2, 3]
    }
}

let array = new ToggleButton(); // [1, 2, 3]

In effect, you can imagine that this is a variable that is defined when super is called and then set to its return value.

const this = super();

The subclass can also return explicitly, and before its call to super() which can return before the constructed instance is made. This is not possible with function constructors because the constructed instance is always created immediately when the subclass constructor is invoked.

class Button {
    constructor () {
        console.log('never called');
    }
}

class ToggleButton extends Button {
    constructor () {
        return [4, 5, 6];
        super(); // not reached, a ToggleButton never created
    }
}

let array = new ToggleButton(); // [4, 5, 6]

If you remember the tip that covered object pooling, the automatic creation of an object that was never used was mentioned as a possible concern. But as long as you’re extending another class, even if its the default Object, you can get around that by returning before super().

class Noodle extends Object { // <-- must use extends to use super
    constructor () {
        if (Noodle.pool.length) {
            return Noodle.pool.pop(); // use pooled instance
        }
        super(); // <-- now new instance created
        // otherwise implicitly returns new instance
    }
    // ...

More info: