JavaScript Objects and Prototypes

JavaScript Objects and Prototypes

The World of Pokemon through the lens of OOPs

JavaScript is an Object Oriented Programming Language. But in a world where OOPs and classes go hand in hand, JavaScript employs a prototype-based object-oriented programming approach. In this article, we will learn about JavaScript objects & prototypes and will use them to implement some OOPs concepts.

What is an object?

According to the OOP paradigm, an object is a real-world entity that we map to our program. In JavaScript, an object is an abstract data type encapsulating various properties under one name(identifier) and each property is represented in the form of key-value pairs. These properties are made to represent the characteristics of real-world entities on which the object is defined. Here is how you declare an object in JavaScript:

let pokemon = {
  name: "Charmander",
  type: "fire",
  level: 5
}

Explanation: The pokemon object has three properties name, type, and level.

image.png These properties can be used to describe pokemon and the pokemon object can be used to access or edit them.

pokemon.name    // access existing property
// O/P- Charmander
pokemon.level += 1;    // edit existing property
// O/P- {
//   name: "Charmander",
//   type: "fire",
//   level: 6
// };
pokemon.hp = 15    // adding new property
// OP- {
//   name: "Charmander",
//   type: "fire",
//   level: 6,
//   hp: 15
// };

Result:

image.png We defined a pokemon named charmander, but in an actual game there will be more than just one pokemon. It would be really difficult to define all of them as separate objects. This is where object constructors come into the picture.

Constructor Function

Object constructors or constructor functions help us define multiple objects with the same set of properties but different values. Let's make a constructor to define our Pokemons.

function Pokemon(name, type, level) {
  this.name = name;
  this.type = type;
  this.level = level;
}

Explanation: The constructor function Pokemon takes in three arguments name, type & level assign values to corresponding object properties. The this keyword is used to access the properties of the current instance of the constructor.

Note: A constructor function must start with a capital letter to distinguish it from other functions.

Instances and this keyword

Now that we have our constructor ready, we can start defining instances of Pokemon. Instances are nothing but the objects we make using a constructor function. We can create instances of Pokemon with the help of new keyword.

let charmandar = new Pokemon("Charamandar", "fire", "5");

Explanation: Here charmander is an instance of Pokemon and the this keyword in the constructor will refer to charmander object for this particular function call. The call will create the following object

{
  "name": "charmander",
  "type": "fire",
  "level": "5"
}

Similarly, we can define more objects with the same constructor:

let charmander = new Pokemon("Charmander", "fire", 5);
let bulbasaur = new Pokemon("Bulbasaur", "grass", 5);
let squirtle = new Pokemon("Squirtle", "water", 5);

Result: image.png

As we add more pokemon to our database, we start having some redundant properties. Take this case for an example, we have three fire-type pokemon charmander, growlithe and rapidash. Each of the three has a separate copy of the property type: "fire", this is called data redundancy and it can affect our memory usage in the long run. To solve this issue we can make this property common to all our fire-types.

Prototypes

An object prototype is a built-in property that allows objects to pass down properties to other objects. Let's create a new constructor called FireType and check its prototype.

function FireType(){
}
console.log(new FireType)

Result: image.png Our object contains a property [[Prototype]] that currently has two sub-properties, constructor which represents the constructor function from which the object was made and another [[Prototype]] which belongs to the parent object of our constructor. This is called prototype chaining and it allows objects to inherit properties from their ancestors( we will use this mechanism in the next section ).

Let's add a property to FireType prototype. The prototype itself is an object so we will modify it just like we would modify any object.

FireType.prototype.type = "fire"
console.log(new FireType)

image.png

The new property was added to the prototype of FireType constructor and can now be used by instances just like any other property.

let charmander = new FireType()
console.log(charmander.type)
// O/P: fire

But unlike other properties, the prototypes can be passed to other constructor functions which in turn allows other objects to use them.

Prototype-based Inheritance

Inheritance is a major concept of Object Oriented Programming, it is the process through which objects pass traits to other objects. The object whose properties are being passed is called the parent and the object inheriting the properties is called the child.

In our case, the Pokemon constructor will be passing down properties to FireType Constructor. We will start redefining the Pokemon constructor

function Pokemon(name, level) {
  this.name = name;
  this.level = level;
}

Now let's take a look at FireType constructor

function FireType(name, level) {
  Pokemon.call(this, name, level);    // invokes the Pokemon constructor
}

Since the FireType inherits from Pokemon constructor, it must intialize name and level properties using the call() method.

Since both the constructors are initialized we can point the prototype of FireType to the prototype of Pokemon.

FireType.prototype = Object.create(Pokemon.prototype);

Object.create allows us to create instances from prototypes. We have assigned the FireType.prototype an instance of Pokemon in order to inherit Pokemon properties.

Now we can add type property to the new assigned prototype. Remember we need only one copy of type property for all FireType instances.

FireType.prototype.type = "fire"

We are almost done, let's check out a FireType object before we move forward.

image.png As expected, the FireType object now has Pokemon's prototype. But there is something missing in this object, can you see it? Yes! the constructor property of FireType is now Pokemon, it was overwritten when we assigned it a new prototype. To fix this we simply need to reassign the constructor property every time we replace a prototype.

FireType.prototype.constructor = FireType

image.png The FireType now has Pokemon's prototype. Think of it as a chain of prototypes where at each node new properties are added and/or modified. image.png We can finally make constructors for other types like grass and water.

// Grass Type 
function GrassType(name, level) {
  Pokemon.call(this, name, level);    
}
GrassType.prototype = Object.create(Pokemon.prototype);
GrassType.prototype.constructor = GrassType
GrassType.prototype.type = "grass"    // common type(grass)
// Water Type
function WaterType(name, level) {
  Pokemon.call(this, name, level);    
}
WaterType.prototype = Object.create(Pokemon.prototype);
WaterType.prototype.type = "water"    // common type(water)
WaterType.prototype.constructor = WaterType

New objects can be created with these constructors

let charmander = new FireType("Charmander", 5)
let charmeleon = new FireType("Charmeleon", 16)
let oddish = new GrassType("Oddish", 5)
let poliwhirl = new WaterType("Poliwhirl", 25)
// list goes on...

In the end, we will have a data flow like this something like this:

image.png

JavaScript provides some abstraction over prototypes using the Class Template which was introduced in 2015 under ES6. The template under the hood uses prototypes to get things done which is why JavaScript is not a class-based language.

You made it! congrats on completing the article. Hopefully, you feel better about objects and prototypes by now. Do comment your suggestions below. I'll see you in the next post!

Cover Photo