Marcell Ciszek Druzynski

Prototype in JavaScript

Prototoype is the way in JavaScript to share methods across all instances of a function. It is commonly used to create new objects in JavaScript.

April 23, 2023

In this article, we will delve into the concept of prototypes in JavaScript and they’re functionality. Gaining a solid understanding of prototypes enables more efficient code reuse by moving away from less performant methods of object creation and method sharing, and instead adopting the use of prototypes. This evolution typically culminates in the utilisation of classes, which make use of JavaScript prototypes behind the scenes. Finally, we will touch on important considerations when working with objects and they’re prototypes.

This post is written and inspired from uidottv I highly recommend that you visit uidottv and check out all their content. They are doing an exceptional job!

Learning how to handle objects in JavaScript is crucial, and this post aims to present various patterns for creating new objects. By doing so, we hope to provide a better understanding of the mental model and the functioning of objects and what the prototype is and how it works in JavaScript.

The most prevalent way of creating a new object in JavaScript is by using an object literal. To add properties to the object, we can use dot notation as demonstrated below:

1const footBallPlayer = {};
2footBallPlayer.name = "Ronaldo";
3footBallPlayer.number = 7;
4footBallPlayer.team = "Real madrid";
1const footBallPlayer = {};
2footBallPlayer.name = "Ronaldo";
3footBallPlayer.number = 7;
4footBallPlayer.team = "Real madrid";

This is a familiar and commonly used technique. To add methods or different behaviour’s to our object, we can implement the following:

1footBallPlayer.getName = function () {
2 return this.name;
3};
4
5footBallPlayer.changeNumber = function (newNumber) {
6 this.number = newNumber;
7};
1footBallPlayer.getName = function () {
2 return this.name;
3};
4
5footBallPlayer.changeNumber = function (newNumber) {
6 this.number = newNumber;
7};

We can add new properties to our objects by using functions. However, it's likely that we'll need to create similar objects for various football players.

To address this issue, we can encapsulate the logic in a function that allows us to generate multiple football players dynamically.

1function FootBallPlayer(name, number) {
2 const player = {};
3 player.name = name;
4 player.number = number;
5
6 player.getName = function () {
7 console.log(`My name is ${this.name}`);
8 return this.name;
9 };
10
11 player.getNumber = function () {
12 console.log(`My number is ${this.number}`);
13 return this.number;
14 };
15
16 player.setNewNumber = function (newNumber) {
17 console.log(`My new number is ${newNumber}`);
18 this.number = newNumber;
19 };
20
21 return player;
22}
23
24const ronaldo = FootBallPlayer("Ronaldo", 7);
25const zidane = FootBallPlayer("Zidane", 10);
1function FootBallPlayer(name, number) {
2 const player = {};
3 player.name = name;
4 player.number = number;
5
6 player.getName = function () {
7 console.log(`My name is ${this.name}`);
8 return this.name;
9 };
10
11 player.getNumber = function () {
12 console.log(`My number is ${this.number}`);
13 return this.number;
14 };
15
16 player.setNewNumber = function (newNumber) {
17 console.log(`My new number is ${newNumber}`);
18 this.number = newNumber;
19 };
20
21 return player;
22}
23
24const ronaldo = FootBallPlayer("Ronaldo", 7);
25const zidane = FootBallPlayer("Zidane", 10);

With this implementation, we can now generate multiple football players dynamically without hardcoding their values. It's an excellent solution!

However, there's an issue: for each football player we create, we create all their specific methods. This approach consumes a considerable amount of memory and is not an efficient way of writing code. Although it may not be a problem for creating two objects in this example, consider building an entire football manager application with a massive number of football player objects and related objects. This approach would consume a significant amount of memory.

How can we solve this problem?

We need to find a way to store our methods in one object, which we can then reference to retrieve the necessary methods. By doing so, we'll only create the methods once, and all football player objects will reference them when called.

Consider the following solution:

1const footBallplayerMethods = {
2 getName() {
3 console.log(`My name is ${this.name}`);
4 return this.name;
5 },
6 getNumber() {
7 console.log(`My number is ${this.number}`);
8 return this.number;
9 },
10 setNewNumber(newNumber) {
11 console.log(`My new number is ${newNumber}`);
12 this.number = newNumber;
13 },
14};
15
16function FootBallPlayer(name, number) {
17 const footBallPLayer = {};
18 footBallPLayer.name = name;
19 footBallPLayer.number = number;
20 footBallPLayer.getName = footBallplayerMethods.getName;
21 footBallPLayer.getNumber = footBallplayerMethods.getNumber;
22 footBallPLayer.setNewNumber = footBallplayerMethods.setNewNumber;
23 return footBallPLayer;
24}
1const footBallplayerMethods = {
2 getName() {
3 console.log(`My name is ${this.name}`);
4 return this.name;
5 },
6 getNumber() {
7 console.log(`My number is ${this.number}`);
8 return this.number;
9 },
10 setNewNumber(newNumber) {
11 console.log(`My new number is ${newNumber}`);
12 this.number = newNumber;
13 },
14};
15
16function FootBallPlayer(name, number) {
17 const footBallPLayer = {};
18 footBallPLayer.name = name;
19 footBallPLayer.number = number;
20 footBallPLayer.getName = footBallplayerMethods.getName;
21 footBallPLayer.getNumber = footBallplayerMethods.getNumber;
22 footBallPLayer.setNewNumber = footBallplayerMethods.setNewNumber;
23 return footBallPLayer;
24}

Instead of recreating each method, we can refer to the footBallplayerMethods object to access the required method. This approach solves the problem of creating multiple methods that perform the same task since they are only created once in memory.

However, this solution may not be the optimal approach. Can you identify a reason why the current implementation may not be the best way to solve the problem?

If we add a new method to the footBallplayerMethods object, we would need to remember to reference it in the FootBallPlayer or any other constructor functions that we've created, if we have any. Here's an example how it could look when adding new methods:

1const footBallplayerMethods = {
2 // ... previous implementation
3 danceWhenScore() {
4 console.log(`I am dancing when I score`);
5 },
6};
7
8function FootBallPlayer(name, number) {
9 // ... previous implementation
10 footBallPLayer.danceWhenScore = footBallplayerMethods.danceWhenScore;
11 return footBallPLayer;
12}
1const footBallplayerMethods = {
2 // ... previous implementation
3 danceWhenScore() {
4 console.log(`I am dancing when I score`);
5 },
6};
7
8function FootBallPlayer(name, number) {
9 // ... previous implementation
10 footBallPLayer.danceWhenScore = footBallplayerMethods.danceWhenScore;
11 return footBallPLayer;
12}

However, what we really want is for FootBallPlayer to always reference footBallplayerMethods, so that we can use any new methods we add to footBallplayerMethods without any extra work. This can be achieved with a feature in JavaScript called Object.create().

Let’s give an example how Object.create() works.

1const banana = {
2 name: "banana",
3 color: "yellow",
4 tasty: 5,
5};
6const mango = Object.create(banana);
1const banana = {
2 name: "banana",
3 color: "yellow",
4 tasty: 5,
5};
6const mango = Object.create(banana);

In this example, we create two objects, a banana object and a mango object using Object.create(). The mango object is a brand new object, which we can confirm by logging it to the console and seeing that it's empty:

1console.log("mango", mango); // {}
1console.log("mango", mango); // {}

However, since we used Object.create() and passed in the banana object, when we try to access a property like mango.color, it doesn't exist on the mango object. Instead, it goes up to the parent object (banana) and uses the color property from there:

1console.log("mango", mango.foo); // undefined
2console.log("mango", mango.color); // yellow
3console.log("mango", mango.tasty); // 5
1console.log("mango", mango.foo); // undefined
2console.log("mango", mango.color); // yellow
3console.log("mango", mango.tasty); // 5

The first console.log() call will return undefined because foo doesn't exist on the mango object, and it doesn't exist on the banana object either, so we get undefined.

We can use a similar approach to refactor our FootBallPlayer object by creating it with Object.create() and adding new methods to the footBallplayerMethods object. We can then reference these methods in the FootBallPlayer object without modifying the FootBallPlayer every time we need to refer to the new method.

1const footBallplayerMethods = {
2 getName() {
3 return this.name;
4 },
5 getNumber() {
6 return this.number;
7 },
8 setNewNumber(newNumber) {
9 this.number = newNumber;
10 },
11 danceWhenScore() {
12 console.log(`${this.name} is dancing when I score`);
13 },
14};
15
16function FootBallPlayer(name, number) {
17 const player = Object.create(footBallplayerMethods);
18 player.name = name;
19 player.number = number;
20 return player;
21}
22
23const ronaldo = FootBallPlayer("Ronaldo", 7);
24const zidane = FootBallPlayer("Zidane", 10);
25
26ronaldo.getName(); // My name is Ronaldo
27ronaldo.getNumber(); // My number is 7
28ronaldo.setNewNumber(10); // My new number is 10
29ronaldo.getNumber(); // My number is 10
30zidane.danceWhenScore(); // Zidane is dancing when I score
1const footBallplayerMethods = {
2 getName() {
3 return this.name;
4 },
5 getNumber() {
6 return this.number;
7 },
8 setNewNumber(newNumber) {
9 this.number = newNumber;
10 },
11 danceWhenScore() {
12 console.log(`${this.name} is dancing when I score`);
13 },
14};
15
16function FootBallPlayer(name, number) {
17 const player = Object.create(footBallplayerMethods);
18 player.name = name;
19 player.number = number;
20 return player;
21}
22
23const ronaldo = FootBallPlayer("Ronaldo", 7);
24const zidane = FootBallPlayer("Zidane", 10);
25
26ronaldo.getName(); // My name is Ronaldo
27ronaldo.getNumber(); // My number is 7
28ronaldo.setNewNumber(10); // My new number is 10
29ronaldo.getNumber(); // My number is 10
30zidane.danceWhenScore(); // Zidane is dancing when I score

In this scenario, calling ronaldo.getName() involves checking if the Ronaldo object has a method named getName(). Since it doesn't, the JavaScript engine searches in the parent object to see if getName() exists there. It finds it, and thus uses getName() from the parent object, which is convenient because similar constructor functions with the same behavior can be created using Object.create().

However, the functionality can be further improved with the use of JavaScript's prototype.

What exactly is the prototype in JavaScript? It is simply a property that every function created in JavaScript has, which points to an object. That's all there is to it - it does not need to be more complex than that. The prototype is a property on a function that points to an object.

Rather than managing the footBallplayerMethods, we can place all of its methods on the prototype. Let's proceed with the refactoring!

1function FootBallPlayer(name, number) {
2 const player = Object.create(FootBallPlayer.prototype);
3 player.name = name;
4 player.number = number;
5 return player;
6}
7
8FootBallPlayer.prototype.getName = function () {
9 return this.name;
10};
11FootBallPlayer.prototype.getNumber = function () {
12 return this.number;
13};
14
15FootBallPlayer.prototype.setNewNumber = function (newNumber) {
16 this.number = newNumber;
17};
18
19FootBallPlayer.prototype.danceWhenScore = function () {
20 console.log(`${this.name} is dancing when he score`);
21};
22
23const ronaldo = FootBallPlayer("Ronaldo", 7);
24const zidane = FootBallPlayer("Zidane", 10);
25
26ronaldo.getName(); // Ronaldo
27ronaldo.getNumber(); // 7
28ronaldo.setNewNumber(10);
29ronaldo.getNumber(); // 10
30zidane.danceWhenScore(); // Zidane is dancing when he score
1function FootBallPlayer(name, number) {
2 const player = Object.create(FootBallPlayer.prototype);
3 player.name = name;
4 player.number = number;
5 return player;
6}
7
8FootBallPlayer.prototype.getName = function () {
9 return this.name;
10};
11FootBallPlayer.prototype.getNumber = function () {
12 return this.number;
13};
14
15FootBallPlayer.prototype.setNewNumber = function (newNumber) {
16 this.number = newNumber;
17};
18
19FootBallPlayer.prototype.danceWhenScore = function () {
20 console.log(`${this.name} is dancing when he score`);
21};
22
23const ronaldo = FootBallPlayer("Ronaldo", 7);
24const zidane = FootBallPlayer("Zidane", 10);
25
26ronaldo.getName(); // Ronaldo
27ronaldo.getNumber(); // 7
28ronaldo.setNewNumber(10);
29ronaldo.getNumber(); // 10
30zidane.danceWhenScore(); // Zidane is dancing when he score

When attempting to call the getName() method on the Ronaldo object after the refactoring, it first checks if Ronaldo has the method. Since it does not, it then looks to FootBallPlayer.prototype to see if the method is present there. It finds the method, and proceeds to execute it from the prototype, returning the name of the object.

A prototype is a property that every function you create in JavaScript has that points to an object. That’s all it is, it does not have to be more complicated than that. Prototype is a property on a function that points to an object.

The key takeaway thus far is that the prototype in JavaScript is merely a property that every function possesses, enabling us to share methods across all instances of a function.

When utilising our constructor function FootBallPlayer, we already have the methods available on the prototype, eliminating the need to manage all methods added or changed for other instances that use the constructor function. The method is created once, and we do not need to create a new method for every instance. Instead, whenever an instance needs to call a method that is not present on the current object, it looks up to the prototype and uses the method from there.

However, if you are already a seasoned JavaScript developer, you likely know about the new keyword, which we can employ with const ronaldo = new FootBallPlayer("Ronaldo",7). The difference between using the new keyword and our current example is that JavaScript performs some operations for us under the hood. Using the new keyword looks something like this:

1function FootBallPlayer(name, number) {
2 this.name = name;
3 this.number = number;
4}
1function FootBallPlayer(name, number) {
2 this.name = name;
3 this.number = number;
4}

Here, JavaScript automatically assigns the properties on the this object and returns this from the function. Therefore, by utilising the new keyword, JavaScript can handle many tasks for us, resulting in a cleaner code structure, as demonstrated by the example.

1function FootBallPlayer(name, number) {
2 this.name = name;
3 this.number = number;
4}
5
6FootBallPlayer.prototype.getName = function () {
7 return this.name;
8};
9FootBallPlayer.prototype.getNumber = function () {
10 return this.number;
11};
12
13FootBallPlayer.prototype.setNewNumber = function (newNumber) {
14 this.number = newNumber;
15};
16
17FootBallPlayer.prototype.danceWhenScore = function () {
18 console.log(`${this.name} is dancing when he score`);
19};
20
21const ronaldo = new FootBallPlayer("Ronaldo", 7);
22const zidane = new FootBallPlayer("Zidane", 10);
23
24ronaldo.getName(); // Ronaldo
25ronaldo.getNumber(); // 7
26ronaldo.setNewNumber(10);
27ronaldo.getNumber(); // 10
28zidane.danceWhenScore(); // Zidane is dancing when he score
1function FootBallPlayer(name, number) {
2 this.name = name;
3 this.number = number;
4}
5
6FootBallPlayer.prototype.getName = function () {
7 return this.name;
8};
9FootBallPlayer.prototype.getNumber = function () {
10 return this.number;
11};
12
13FootBallPlayer.prototype.setNewNumber = function (newNumber) {
14 this.number = newNumber;
15};
16
17FootBallPlayer.prototype.danceWhenScore = function () {
18 console.log(`${this.name} is dancing when he score`);
19};
20
21const ronaldo = new FootBallPlayer("Ronaldo", 7);
22const zidane = new FootBallPlayer("Zidane", 10);
23
24ronaldo.getName(); // Ronaldo
25ronaldo.getNumber(); // 7
26ronaldo.setNewNumber(10);
27ronaldo.getNumber(); // 10
28zidane.danceWhenScore(); // Zidane is dancing when he score

If you're experienced in programming, you may have noticed that the code we just created resembles how a class works. Our constructor function returns an object, allowing us to create different instances. However, JavaScript did not have built-in classes for a long time, so this pattern was commonly used. Today, classes in JavaScript are just syntactic sugar built on top of prototypes. Under the hood, they still use prototypes.

So, if we were to rewrite this code using a class, it would look something like this.

1class FootBallPlayer {
2 constructor(name, number) {
3 this.name = name;
4 this.number = number;
5 }
6 getName() {
7 return this.name;
8 }
9 getNumber() {
10 return this.number;
11 }
12 setNewNumber(number) {
13 this.number = number;
14 }
15 danceWhenScore() {
16 console.log(`${this.name} is dancing when he score`);
17 }
18}
19
20const ronaldo = new FootBallPlayer("Ronaldo", 7);
21const zidane = new FootBallPlayer("Zidane", 10);
1class FootBallPlayer {
2 constructor(name, number) {
3 this.name = name;
4 this.number = number;
5 }
6 getName() {
7 return this.name;
8 }
9 getNumber() {
10 return this.number;
11 }
12 setNewNumber(number) {
13 this.number = number;
14 }
15 danceWhenScore() {
16 console.log(`${this.name} is dancing when he score`);
17 }
18}
19
20const ronaldo = new FootBallPlayer("Ronaldo", 7);
21const zidane = new FootBallPlayer("Zidane", 10);

In my opinion, using classes in JavaScript looks much cleaner compared to using the raw prototype pattern. Even though classes are just syntactic sugar, I believe it's perfectly acceptable to use them in JavaScript, as long as you have a good understanding of how they work underneath the surface. That's why we went through all the previous examples - to gain a deep understanding of how new code is written and how objects are linked to each other in JavaScript.

Good things to know

Now that we have a solid understanding of the fundamentals of prototypes, let's explore some important things to know about prototypes in JavaScript.

Have you ever wondered where all those convenient methods like push(), pop(), and map() come from when using arrays in JavaScript? Well, they actually live on the array prototype! When you create a new array like this: const myArr = [], it's just syntactic sugar for using const myArr = new Array(). This is what JavaScript is creating for us behind the scenes.

To see how it looks, I recommend going to your browser console and typing: Array.prototype. You'll see a list of methods displayed that live on the arrays' prototype.

Now, let's revisit our previous examples. How can we access the prototype of our objects? Let's say we want to get the prototype of the Ronaldo object. We can do that simply by using Object.getPrototypeOf().

1const ronaldo = new FootBallPlayer("Ronaldo", 7);
2const proto = Object.getPrototypeOf(ronaldo);
3
4console.log(proto === FootBallPlayer.prototype); // true
5console.log(proto === Array.prototype); // false
1const ronaldo = new FootBallPlayer("Ronaldo", 7);
2const proto = Object.getPrototypeOf(ronaldo);
3
4console.log(proto === FootBallPlayer.prototype); // true
5console.log(proto === Array.prototype); // false

In this example, it is apparent that the prototype of the Ronaldo object is an instance of the FootballPlayer object. The Array prototype is not the same as the FootballPlayer prototype, as the FootballPlayer prototype is an instance of the FootballPlayer object, whereas the Array prototype is an instance of the Array object.

Looping over the object

There are some important points to keep in mind when iterating over objects. It is not recommended to use the for in loop, as it not only retrieves the keys and methods for the current instance, but also includes all the values from the upper prototype. This issue arises when we are utilising the outdated approach of creating prototypes with our own constructor function.

1function FootBallPlayer(name, number) {
2 this.name = name
3 this.number = number
4 ...
5}
6
7...
8FootBallPlayer.prototype.getName = function () {
9 return this.name
10}
11...
1function FootBallPlayer(name, number) {
2 this.name = name
3 this.number = number
4 ...
5}
6
7...
8FootBallPlayer.prototype.getName = function () {
9 return this.name
10}
11...

If we were to iterate over the Ronaldo object using a loop, the following would be displayed in the console:

1name Ronaldo
2number 7
3getName [Function (anonymous)]
4getNumber [Function (anonymous)]
1name Ronaldo
2number 7
3getName [Function (anonymous)]
4getNumber [Function (anonymous)]

We obtain the getName() and getNumber() methods from the FootBallPLayers prototype. To avoid logging the FootBallPLayers prototype, we can utilise the hasOwnProperty() method to verify if the current object possesses the properties on its own prototype and not traverse up the prototype chain.

So if we type:

1console.log(ronaldo.hasOwnProperty("name")); // true
2console.log(ronaldo.hasOwnProperty("getName")); // false
1console.log(ronaldo.hasOwnProperty("name")); // true
2console.log(ronaldo.hasOwnProperty("getName")); // false

As the Ronaldo object possesses a name property, it will output true. However, in the other case where the object does not have the getName() method and it resides on the FootBallPLayers prototype, it will output false.

So in our loop we can now change it to:

1for (const key in ronaldo) {
2 if (ronaldo.hasOwnProperty(key)) {
3 console.log(key, ronaldo[key])
4 }
5}
6
7 // Logs
8 name Ronaldo
9 number 7
10
1for (const key in ronaldo) {
2 if (ronaldo.hasOwnProperty(key)) {
3 console.log(key, ronaldo[key])
4 }
5}
6
7 // Logs
8 name Ronaldo
9 number 7
10

By utilising the hasOwnProperty() method, we can avoid logging the prototype keys from the upper prototype chain, and only retrieve properties that currently exist on the prototype of the current object, in this case the Ronaldo object.

How to check if object is an instance of a class?

There are situations where it can be beneficial to determine if a particular object is an instance of a class. The instanceof operator can be used for this purpose, as demonstrated below:

1class FootBallPlayer {
2 constructor(name, number) {
3 this.name = name;
4 this.number = number;
5 }
6 getName() {
7 return this.name;
8 }
9 getNumber() {
10 return this.number;
11 }
12 setNewNumber(number) {
13 this.number = number;
14 }
15 danceWhenScore() {
16 console.log(`${this.name} is dancing when he score`);
17 }
18}
19
20class Vector {
21 constructor(x, y) {
22 this.x = x;
23 this.y = y;
24 }
25 // ... methods
26}
27
28const messi = new FootBallPlayer("Messi", 10);
29const v1 = new Vector(1, 2);
30console.log(messi instanceof FootBallPlayer); // true
31console.log(v1 instanceof Vector); // true
32console.log(messi instanceof Vector); // false
1class FootBallPlayer {
2 constructor(name, number) {
3 this.name = name;
4 this.number = number;
5 }
6 getName() {
7 return this.name;
8 }
9 getNumber() {
10 return this.number;
11 }
12 setNewNumber(number) {
13 this.number = number;
14 }
15 danceWhenScore() {
16 console.log(`${this.name} is dancing when he score`);
17 }
18}
19
20class Vector {
21 constructor(x, y) {
22 this.x = x;
23 this.y = y;
24 }
25 // ... methods
26}
27
28const messi = new FootBallPlayer("Messi", 10);
29const v1 = new Vector(1, 2);
30console.log(messi instanceof FootBallPlayer); // true
31console.log(v1 instanceof Vector); // true
32console.log(messi instanceof Vector); // false

The expression Messi instanceof FootBallPlayer will yield true, as Messi is an instance of the FootBallPlayer class. However, Messi is not an instance of the Vector class.

It's important to note that when creating constructor functions and generating new instances with the new keyword, arrow functions in JavaScript will not work as expected. Arrow functions do not have their own this context, which means they do not require a prototype property. If you attempt to use an arrow function as a constructor, you will encounter a TypeError with a message similar to Person is not a constructor.

1const Person = (name) => {
2 this.name = name;
3};
4
5const bob = new Person("bob");
1const Person = (name) => {
2 this.name = name;
3};
4
5const bob = new Person("bob");

Summery

In JavaScript, the prototype chain is a mechanism that allows objects to inherit properties and methods from their prototype objects. Every object in JavaScript has a prototype, which serves as a blueprint for defining its behaviour. When a property or method is accessed on an object, JavaScript looks for it in the object itself first, and if it doesn't find it there, it looks for it in the object's prototype. If it still doesn't find it there, it continues to search up the prototype chain until it reaches the top-level object, typically the Object.prototype object.

Objects in JavaScript can be created using constructor functions or object literals, and both methods involve prototype chains. Constructor functions are used to create objects with shared properties and methods, and objects created from the same constructor function share the same prototype object, forming a prototype chain. Object literals also have a prototype, which is the Object.prototype object by default.

The prototype chain allows for efficient code reuse in JavaScript, as objects can inherit properties and methods from their prototype without having to duplicate them in each instance. However, it's important to be mindful of the prototype chain when modifying objects, as changes to a prototype can affect all objects that inherit from it. Additionally, care should be taken when traversing the prototype chain to avoid infinite loops or unexpected behaviour.

Resources