JS Tip of the Day: Constructor Internals

Constructor Internals
Version: ES2015 (class syntax)
Level: Intermediate

When using constructors to create custom object instances, there’s a little bit of magic that happens in the background when the constructor is being used. This is the unwritten behavior that makes constructors useful and do what they do. Here we’ll look at what that magic is for both function and class-based constructors.

Function-based constructors are the simplest of the kinds of constructors. They do a little magic, but not as much as their class counterparts. When you write a function constructor, you’re mostly only responsible for assigning properties to the instance. The creation of the instance itself and the returning of it from the constructor call is handled automatically for you.

Note: All of the following sample code assumes constructors being called with new and denotes internal (or “magic”) behavior with //~ comments.

function User (name) {
    //~ this = Object.create(User.prototype);
    this.name = name;
    //~ return this;
}
//~ User.prototype = { constructor: User };

Here you can see when the function is declared, because its a function that can be used as a constructor, a prototype property is automatically assigned to it. Then, when called as a constructor, a new instance is created based on that prototype, assigned to this, and automatically returned after all user code (here, this.name = name) has run.

One thing function constructors don’t help with much is when extending other constructors. No additional magic is provided; it’s all manual.

function Person () {}
//~ Person.prototype = { constructor: Person };

function User (name) {
    //~ this = Object.create(User.prototype);
    Person.call(this); // manual super()
    this.name = name;
    //~ return this;
}
//~ User.prototype = { constructor: User };

// manual extends
User.prototype = Object.create(Person);
User.prototype.constructor = User;

The class syntax offers a new way to write constructors in JavaScript. The end result is largely the same - a function you use to create new object instances, but class offers features not available in function-based constructors, like the use of super.

When it comes to class base classes, you have help similar to that found in function-based constructors.

class User {
    constructor (name) { // function representing User
        //~ this = Object.create(User.prototype);
        this.name = name;
        //~ return this;
    }
}
//~ User.prototype = { constructor: User };

But once you extend another class, you start to see all the benefits that were missing with function constructors.

class Person {
    //~ constructor () {
        //~ this = Object.create(new.target.prototype);
        //~ return this;
    //~ }
}
//~ Person.prototype = { constructor: Person };

class User extends Person {
    constructor (name) {
        super(); // translates to:
        //~ this = new Object.getPrototypeOf(User);

        this.name = name;
        //~ return this;
    }
}
//~ User.prototype = Object.create(Person.prototype);
//~ User.prototype.constructor = User;
//~ Object.setPrototypeOf(User, Person);

Starting at the bottom, you can see the extending code is set up automatically. In addition to setting up the prototype chain, it also includes setting up static inheritance by having the constructor inherit directly from the constructor it’s extending.

Inside the constructor, when super() is called, a call is made to the constructor that the current constructor inherits from, which in this case would be Parent. This happens dynamically so it’s possible that it would not be the same constructor in the extends clause (only if you, or someone else, was messing around changing static inheritance before construction). In either case, as seen in the Person constructor, the object created is created with a prototype based on the new.target which is the function that new was called with, or in this case User. The actual object instance creation step only happens in the base class, or the last class in the super chain. Every other superclass that would be in between would get their instance from their own super() call until eventually reaching the new.target class which, in turn, ultimately passes it back to the caller.

It’s all this extra work that makes class syntax much more appealing when defining constructors. All of the boilerplate code that you had to do before, notably when extending, is taken care of for you.

Theres a bit more magic with constructors not touched upon here. That includes (but may not be limited to):

  • For any constructor, if you return explicitly out of a constructor, as long as you’re returning an object, the automatic this return is ignored.
  • class constructors throw an error if you attempt to call them as a function. This is a little different than adding a throw in the constructor itself (purposefully not included in code examples above) since this error would happen before the constructor is ever run.
  • if a constructor method isn’t defined for a class, one is created for you automatically, constructor(){} if not extending another class and constructor(...args){ super(...args); } if it is.
  • Only class constructors can properly extend builtins that allow created instances to be exotic if necessary (e.g. extending Array) and have all of their necessary internal slots initialized.

More info: