4

Before this is marked as a duplicate I've read What is the purpose of setting prototype.constructor and Why is it necessary to set the prototype constructor? and still don't understand this. Furthermore one of these posts is from 2011 so I don't know whether it's still relevant.

I'm taking a JavaScript course - covering both ES5 and ES6 - which gives the following as an example:

// Person constructor
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// Greeting
Person.prototype.greeting = function(){
    return `Hello there ${this.firstName} ${this.lastName}`;
}

The next part describes "Prototype Inheritance". The tutor goes on to create Customer and inherits the greeting function from Person:

// Customer constructor
function Customer(firstName, lastName, phone, membership) {
    Person.call(this, firstName, lastName);

    this.phone = phone;
    this.membership = membership;
}

// Inherit the Person prototype methods
Customer.prototype = Object.create(Person.prototype);

So far this makes sense.

The next bit I don't understand. They have added the following line:

// Make customer.prototype return Customer()
Customer.prototype.constructor = Customer;

This seems to be what's described in the linked SO posts, although I can't find the explanation as to why this is actually needed. The next part of the code is:

// Create customer
const customer1 = new Customer('Tom', 'Smith', '555-555-5555', 'Standard');

// Customer greeting
Customer.prototype.greeting = function(){
    return `Hello there ${this.firstName} ${this.lastName} welcome to our company`;
}

console.log(customer1.greeting());

Which logs:

Hello there Tom Smith welcome to our company

If I comment-out the line

//Customer.prototype.constructor = Customer;

And then console.log(customer1) it's giving:

enter image description here

In the __proto__ it's saying Person.

But if I uncomment the line

Customer.prototype.constructor = Customer;

And re-execute, it's giving this in __proto__:

enter image description here

I can see the constructor is referencing Customer. However, what's the point of this given the output is the same in both cases?

The accepted answer on Why is it necessary to set the prototype constructor? doesn't really address this. It's going on about whether you get true/false from checking instanceof. Well, so what? What's the actual reason for using this? Another answer even says

"It isn't necessary to set the prototype."

I really can't understand why this is a thing in the course if there's essentially no need in doing it.

With respect to this particular code is there any situation under which it might be needed?

At the end of the course video covering this topic the presenter says

"Any prototype method we add to the Person will now be accessible through the Customer."

Well - it seems to me that - this is the case anyway, regardless of whether you use prototype.constructor. Which further adds to the confusion about its purpose.

Andy
  • 5,142
  • 11
  • 58
  • 131
  • 2
    I do believe [Why is it necessary to set the prototype constructor?](https://stackoverflow.com/questions/8453887/why-is-it-necessary-to-set-the-prototype-constructor) addresses exactly your query. Basically, if you *don't* overwrite the constructor back to the "correct" one you might run into problems if you try to dynamically get the constructor from an instance, e.g., `variable.constructor` would not produce the correct result. Whether or not you're going to run into that is another manner but I suppose it's better to be safe than sorry. – VLAZ Jan 13 '20 at 11:10
  • @VLAZ thanks for that. It would be really helpful if you could write out an answer which uses the code I've put in. I've tried to put effort into writing this up because none of the answers clearly explain or demonstrate what you're talking about. Saying "might" run into problems means people are likely to just ignore / not bother with understanding the issue, especially if they're not actually encountering differences in the output – Andy Jan 13 '20 at 11:12

1 Answers1

4

First of all, what is the constructor property for in the prototype? It's going to sound dumb but...it gives you the constructor that created this object. So, exactly what you might imagine. However, perhaps a better question is why you'd want to do that? Well, it's actually hard to say. It's not very often you might want a constructor but it could have its uses. I'd come back to this.

Now, what happens if the constructor is not overwritten? A problem might arise if you try to dynamically fetch the constructor of some variable. Let's suppose you want to have something that copies objects. Here is a rather simplified example - we try to copy and we expect unary constructor functions that will always expect a string:

function copier(instance) {
  const constructor = instance.constructor;

  const copy = new constructor("clone");

  return copy;
}

This lets us take any object and produce a basic clone of it.

Now, let's simplify the code you have, avoid overwriting the constructor property and use it with copier:

function Person(name) {
    this.name = name;
    this.member = false;
}

function Customer(name) {
    Person.call(this, name);
    this.member = true;
}

Customer.prototype = Object.create(Person.prototype);

function copier(instance) {
  const constructor = instance.constructor;

  const copy = new constructor("clone");

  return copy;
}

//instantiate some objects
const a = new Person("Alice");
console.log(a);

const b = new Customer("Bob");
console.log(b);

//later on we copy some object we get
const c = copier(b);

console.log(c);//c.member = false, even if b.member = true
console.log("c instanceof Person", c instanceof Person); //true
console.log("c instanceof Customer", c instanceof Customer); //false

console.log("b instanceof Customer", b instanceof Customer);//true
console.log("c instanceof b.constructor", c instanceof b.constructor); //true

And here is where the problem arises. We copied c from b and yet while b is a Customer instance, c is not. It's ultimately because b.constructor === Customer is false which leads to c (the copy of b) being incorrect and running through the wrong construction logic.

This was a rather simplified example to show how you might have a problem. Whether or not you'd have that depends on your usage of the constructor property. In a lot of cases, you maybe don't care. However, you could also design your systems to be highly dynamic and clone generic objects. It's hard to give a generic sort of example when that would be useful but it might be. If you want to produce an easy clone functionality:

MyObject.prototype.function clone() {
   return this.constructor();
}

Then you'd need the constructor to be set correctly, or cloning won't work.

A slightly realistic example

Here is an actual more realistic example. It's still going to be simplified for the sake of illustrating the constructor property but hopefully it doesn't seem too contrived.

Here is the scenario - we want to create a shooter came game. It's viewed from the top and movement is on a grid you can go up, down, left, right, and the between directions, 8 in total. We want to integrate a some splitting where some of the game objects can turn into several. It's like cloning but we produce mroe than just one extra:

  • a two way split, where the object essentially does a Y split, producing two copies of itself (original ceases to exist). So if the projectile is moving "up", it produces a \/ pattern after the split.
  • a three way split that is similar to the above but produces three copies of itself. if moving up it produces \|/ pattern.
  • an eight-way split going in all directions.

Quick overview of how the setup would look like:

/**
 * @param x - left/right position on the game map
 * @param y - up/down position on the game map
 * @param direction - where the object is moving. For simplicity, 
 *   let's assume degrees but only for 8 cardinal directions: 0, 45, 90, 135, 180, 225, 270, 315, 360
     also assume the underflows and overflows are normalised
 * @param velocity - speed of movement of object.
 */
function GameObject(x, y, direction, velocity) {
  this.x = x;
  this.y = y;
  this.direction = direction;
  this.velocity = velocity;
}

//our main types of actors in the game. Assume each has extra behaviour.
function Player()      { GameObject.apply(this, arguments) }
function Enemy()       { GameObject.apply(this, arguments) }
function Projectile()  { GameObject.apply(this, arguments) }

//the splitting mechanics we define

//remnants go in two directions 90 degrees from one another
function split2() {
   return [
     new this.constructor(this.x, this.y, this.direction - 45, this.velocity),
     new this.constructor(this.x, this.y, this.direction + 45, this.velocity)
   ]
}

//remnants go in three directions 45 degrees from one another
function split3() {
   return [
     new this.constructor(this.x, this.y, this.direction - 45, this.velocity),
     new this.constructor(this.x, this.y, this.direction,      this.velocity),
     new this.constructor(this.x, this.y, this.direction + 45, this.velocity)
   ]
}

//remnants go in all directions, 45 degrees from one another
function split8() {
   return [
     new this.constructor(this.x, this.y, 0, this.velocity),
     new this.constructor(this.x, this.y, 45, this.velocity),
     new this.constructor(this.x, this.y, 90, this.velocity),
     new this.constructor(this.x, this.y, 135, this.velocity),
     new this.constructor(this.x, this.y, 180, this.velocity),
     new this.constructor(this.x, this.y, 225, this.velocity),
     new this.constructor(this.x, this.y, 270, this.velocity),
     new this.constructor(this.x, this.y, 315, this.velocity),
   ]
}

Now, let's add some different enemies and weapon projectiles. Some enemies might be able to split and produce more enemies, as would some weapon projectiles. I'll take some loose inspiration from a game called Crimsonland for this example.

function Spider() { Enemy.apply(this, arguments) }
function Zombie() { Enemy.apply(this, arguments) }

function SplitterProjectile     { Projectile.apply(this, arguments) }
function PlasmaCannonProjectile { Projectile.apply(this, arguments) }
function MultiPlasmaProjectile  { Projectile.apply(this, arguments) }

And apply splitting behaviour to them. Most of it is not infinite but let's assume the limit of the splitting is handled elsewhere so we can just focus on the example:

  • Spiders spawn 8 more spiders after they die. This happens only once per spider, children don't split.

  • Zombies spawn two zombies on death. Also once.

  • the Splitter gun shoots projectiles that can do a Y split upon hitting a target. Each child can also split. Up to 8 generations of child projectiles can continue splitting, generation 9 stops.

  • the Plasma Cannon shoots one projectile that splits into many upon hitting an enemy. This happens once.

  • the Multi-Plasma gun actually shoots three projectiles at once, it doesn't split on impact. However, we can easily model the behaviour by calling split on shooting.

Spider.prototype.split = split8;
Zombie.prototype.split = split2;

SplitterProjectile.prototype.split = split2;
PlasmaCannonProjectile.prototype.split = split8;
MultiPlasmaProjectile.prototype.split = split3;

And we have the skeleton code for out entities in the game. If the constructors are wrong, now nothing would work - we wouldn't get a spider produce 8 other spiders but unknown generic entities without specific Spider behaviour to them. Same with Zombies. And projectiles will produce non-specific objects instead of more projectiles. Overall, nothing would work.

Community
  • 1
  • 1
VLAZ
  • 26,331
  • 9
  • 49
  • 67