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: