JS Tip of the Day: When the constructor Property Matters

When the constructor Property Matters
Level: Advanced

When using inheritance with function-based constructors, you’ll often see that when setting up the prototype, the constructor property of the new prototype is reassigned to the constructor function getting the new prototype.

function Scientist () {}
function Dexter () {}

// extends
Dexter.prototype = Object.create(Scientist.prototype);
Dexter.prototype.constructor = Dexter; // <- fix constructor

This is done because the original constructor property within the prototype gets overwritten when the prototype is replaced with a new object. If you didn’t do this, checking the constructor property of instances of that constructor would be incorrect.

function Scientist () {}
function Dexter () {}

// extends
Dexter.prototype = Object.create(Scientist.prototype);

let dexter = new Dexter();
console.log(dexter.constructor); // Scientist (expected Dexter)

This fix, however, is not entirely necessary. The inner workings of JavaScript will not fail to create the instances from that constructor or do anything else incorrectly as a result of the constructor property referencing the wrong constructor in the example above. If you were to forget to fix the constructor property in this way (as seen above), you would probably never notice unless you had any reason to refer to that property directly yourself.

There are only a few cases where the value of constructor really does matter to JavaScript (as of ES2019). The first, and probably most pervasive, is with default species values.

For types that support species, the instance methods that acquire the species are going to be checking for the Symbol.species value on an instance’s constructor property. If constructor is wrong, any resulting species-created instance may be the wrong type. Common types that support and actively use species includes Array and Promise.

class MyArray extends Array {}
let array = new MyArray(1,2,3);
console.log(array.map(x => x) instanceof MyArray); // true

class OtherArray extends Array {}
array.constructor = OtherArray;
console.log(array.map(x => x) instanceof MyArray); // false
console.log(array.map(x => x) instanceof OtherArray); // true
class MyPromise extends Promise {}
let promise = MyPromise.resolve();
console.log(promise.then(x => x) instanceof MyPromise); // true

class OtherPromise extends Promise {}
promise.constructor = OtherPromise;
console.log(promise.then(x => x) instanceof MyPromise); // false
console.log(promise.then(x => x) instanceof OtherPromise); // true

Additionally, promises also check constructor values when using Promise.resolve(). If the value being resolved is already a promise of the same type from which resolve() is called - as determined by the value of it’s constructor property - instead of creating a new promise wrapping that value, that same promise is returned. If they do not match, a new promise instance is created of the promise type of the caller of resolve().

class MyPromise extends Promise {}
let orig = MyPromise.resolve();
let resolved = MyPromise.resolve(orig);
console.log(resolved === orig); // true

class OtherPromise extends Promise {}
orig.constructor = OtherPromise;
resolved = MyPromise.resolve(orig);
console.log(resolved === orig); // false
console.log(resolved instanceof MyPromise); // true
console.log(resolved instanceof OtherPromise); // false

Note that the change in constructor here didn’t change the kind of instance created by resolve() (MyPromise.resolve() still creates MyPromise instances), just that a new instance is returned rather than the original.

As with Promise.resolve(), RegExp, when used as a factory function (not using new), may also return the instance provided rather than creating a new instance from scratch. It too looks to the constructor property to see if there’s a match, and if not, a new RegExp instance is created.

let orig = /orig/;
let created = RegExp(orig);
console.log(created === orig); // true

class OtherRegExp extends RegExp {}
orig.constructor = OtherRegExp;
created = RegExp(orig);
console.log(created === orig); // false
console.log(created instanceof RegExp); // true
console.log(created instanceof OtherRegExp); // false

One thing you may have noticed in these examples is that class was used to create alternate types, not normal constructor functions. That’s because each of the types we were working with are either exotic (Array) or have internal slots (Promise, RegExp) that class is able to correctly handle during initialization. It is unlikely you’d be extending these types with normal constructor functions so ultimately, in the end, there shouldn’t be much concern around not updating the constructor property in these cases (as class performs the constructor fix for you when extending). That’s not to say you shouldn’t do it, though, assuming you’re even using function constructors (class syntax takes care of it all for you).

More info: