Every constructor has a prototype property, which will become the instance's [[Prototype]] when called via the new operator. ConstructorFunction.prototype.constructor will therefore become a property on the instance's [[Prototype]], as previously demonstrated.
However, if ConstructorFunction.prototype is re-assigned, the constructor property will be lost. For example, the following is a common way to create an inheritance pattern:
function Parent() {
}
Parent.prototype.parentMethod = function () {};
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
The constructor of instances of Child will be Parent due to Child.prototype being re-assigned.
This is usually not a big deal — the language almost never reads the constructor property of an object. The only exception is when using @@species to create new instances of a class, but such cases are rare, and you should be using the extends syntax to subclass builtins anyway.
However, ensuring that Child.prototype.constructor always points to Child itself is crucial when some caller is using constructor to access the original class from an instance. Take the following case: the object has the create() method to create itself.
function Parent() {
}
function CreatedConstructor() {
Parent.call(this);
}
CreatedConstructor.prototype = Object.create(Parent.prototype);
CreatedConstructor.prototype.create = function () {
return new this.constructor();
};
new CreatedConstructor().create().create();
In the example above, an exception is thrown, since the constructor links to Parent. To avoid this, just assign the necessary constructor you are going to use.
function Parent() {
}
function CreatedConstructor() {
}
CreatedConstructor.prototype = Object.create(Parent.prototype, {
constructor: {
value: CreatedConstructor,
enumerable: false,
writable: true,
configurable: true,
},
});
CreatedConstructor.prototype.create = function () {
return new this.constructor();
};
new CreatedConstructor().create().create();
Note that when manually adding the constructor property, it's crucial to make the property non-enumerable, so constructor won't be visited in for...in loops — as it normally isn't.
If the code above looks like too much boilerplate, you may also consider using Object.setPrototypeOf() to manipulate the prototype chain.
function Parent() {
}
function CreatedConstructor() {
}
Object.setPrototypeOf(CreatedConstructor.prototype, Parent.prototype);
CreatedConstructor.prototype.create = function () {
return new this.constructor();
};
new CreatedConstructor().create().create();
Object.setPrototypeOf() comes with its potential performance downsides because all previously created objects involved in the prototype chain have to be re-compiled; but if the above initialization code happens before Parent or CreatedConstructor are constructed, the effect should be minimal.
Let's consider one more involved case.
function ParentWithStatic() {}
ParentWithStatic.startPosition = { x: 0, y: 0 };
ParentWithStatic.getStartPosition = function () {
return this.startPosition;
};
function Child(x, y) {
this.position = { x, y };
}
Child.prototype = Object.create(ParentWithStatic.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true,
},
});
Child.prototype.getOffsetByInitialPosition = function () {
const position = this.position;
const startPosition = this.constructor.getStartPosition();
return {
offsetX: startPosition.x - position.x,
offsetY: startPosition.y - position.y,
};
};
new Child(1, 1).getOffsetByInitialPosition();
For this example to work properly, we can reassign the Parent's static properties to Child:
Object.assign(Child, ParentWithStatic);
Child.prototype = Object.create(ParentWithStatic.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true,
},
});
But even better, we can make the constructor functions themselves extend each other, as classes' extends do.
function ParentWithStatic() {}
ParentWithStatic.startPosition = { x: 0, y: 0 };
ParentWithStatic.getStartPosition = function () {
return this.startPosition;
};
function Child(x, y) {
this.position = { x, y };
}
Object.setPrototypeOf(Child.prototype, ParentWithStatic.prototype);
Object.setPrototypeOf(Child, ParentWithStatic);
Child.prototype.getOffsetByInitialPosition = function () {
const position = this.position;
const startPosition = this.constructor.getStartPosition();
return {
offsetX: startPosition.x - position.x,
offsetY: startPosition.y - position.y,
};
};
console.log(new Child(1, 1).getOffsetByInitialPosition());
Again, using Object.setPrototypeOf() may have adverse performance effects, so make sure it happens immediately after the constructor declaration and before any instances are created — to avoid objects being "tainted".
Note: Manually updating or setting the constructor can lead to different and sometimes confusing consequences. To prevent this, just define the role of constructor in each specific case. In most cases, constructor is not used and reassigning it is not necessary.