const - JS | lectureJavaScript OOP

Prototypes

JavaScript OOP

We talked about prototypes, prototypal inheritance, and delegation in the previous lectures. But how does all of this work in JavaScript? Well, before we get into the details, we can summarize the concept of prototypes in JavaScript as follows:

  • Each and every function in JavaScript automatically has a property called prototype. And that includes constructor functions.
  • Every object that's created by a certain constructor function will have access to all the methods and properties that are defined on the prototype property of that constructor function. (Person.prototype from the previous lecture)

Let's now actually add a method to the prototype property of the Person constructor function. The prototype property is an object, so we can write:

index.js
Person.prototype.calculateAge = function () {
  console.log(2024 - this.birthYear);
};

console.log(Person.prototype);

Person prototype

So, based on what we said earlier, which is that every object that's created by the Person constructor function will get access to all the methods defined on the prototype property of that constructor function, we should be able to call the calculateAge method on the evan object, right?

index.js
evan.calculateAge();

Prototype Method Call

It works! So, we can now use the calculateAge method on the evan object even though we didn't define it on the object itself.

Evan Object

From above, we can see that our evan object doesn't have a calculateAge method. But still, it has access to it because of prototypal inheritance. Beautiful! So, this solution effectively solves the problem we had when we added the calculateAge method directly to each of the objects.

index.js
// Previous Lecture - don't do this!
const Person = function (firstName, lastName, birthYear, profession) {
  // Instance properties
  this.firstName = firstName;
  //...

  this.calculateAge = function () {
    console.log(2024 - this.birthYear);
  };
};

Once again, with this above solution from the previous lecture, we would have created a copy of the calculateAge method and attached it to each and every object that's created by the Person constructor function. And so that's why we don't do that. Instead, we add the method directly to the prototype property of the constructor function so that it's only created once and then shared across all the objects that are created by that constructor function.

In a nutshell, this is how we implement very basic prototypal inheritance in JavaScript. So, we just observed that the evan object is kind of somehow connected to the Person constructor function. That's why it can have access to the calculateAge method located inside the prototype property of the Person constructor function. But how and why does this work? Well, it works because any object always has access to the methods and properties from its prototype. And the prototype of evan is Person.prototype.

We can actually confirm that because each object has a special property called __proto__.

index.js
console.log(evan.__proto__);

Evan Prototype

From the result we can clearly see the calculateAge method, and that's the reason why evan is able to use it. So, the prototype of the evan object is basically the prototype property of the constructor function.

index.js
console.log(evan.__proto__ === Person.prototype);

Evan Prototype

However, there's one more thing we need to understand. The prototype property of the constructor function is not the prototype of the constructor function itself. It's the prototype of all the objects that are created by the constructor function. It is a subtle but important distinction.

Let's use another built in method to prove this.

index.js
console.log(Person.prototype.isPrototypeOf(evan));
console.log(Person.prototype.isPrototypeOf(Person));

Person Prototype

The next question you are surely asking yourself is: "Where does the __proto__ property on the evan object come from?" Well, do you remember what step number three does when we use the new operator? It links the newly created empty object to the prototype. And so basically, it is at that moment that the __proto__ property is created. That is, it creates the __proto__ property and links it to the prototype property of the constructor function that is being called.

New Operator

We can also set properties on the prototype property of the constructor function.

index.js
Person.prototype.species = 'Homo Sapiens';

This property will be available on all the objects that are created by the Person constructor function.

index.js
console.log(evan);

Evan Object

index.js
console.log(evan.species); // Homo Sapiens

We can see that this property is not directly on the evan object. So, it's not its own property. Own properties are only the ones that are declared directly on the object itself, not including inherited properties. There exist a method called hasOwnProperty that we can use to check if a property is an own property or not.

index.js
evan.hasOwnProperty('firstName'); // true
evan.hasOwnProperty('species'); // false