Deep Dive into Prototypal Inheritance in JavaScript

Deep Dive into Prototypal Inheritance in JavaScript

Introduction

Prototype

Objects in JavaScript have an internal property known as prototype. It is simply a reference to another object and contains common attributes/properties across all instances of the object.

  • An object’s prototype attribute specifies the object from which it inherits properties.
  • The prototype property is non-enumerable, meaning that it doesn't show up when we try to access the object's properties.

Prototype Chain

When an object gets a request for a property that it does not have, its prototype will be searched for the property, then the prototype’s prototype, and so on. That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype and acts as the final link in this prototype chain.

So what is the prototype of an object? It is the great ancestral prototype, the entity behind almost all objects, Object.prototype. Many objects don’t directly have Object.prototype as their prototype, but instead, have another object that provides a different set of default properties. Functions derive from Function.prototype, and arrays derive from Array.prototype, and so on.

In a prototypal system, objects inherit from objects.

Example

Let's create an object and walk you through an example.

const user = {
    firstName: 'Vishwajeet'
}

console.log(user.firstName) // Vishwajeet ​​​​
console.log(user.lastName) // undefined ​​​​
console.log(user.toString()) // [object Object] ​​​

Accessing a property if it's defined in an object gives you the value of the property. If you try to access a property that doesn't exist in the object you are bound to get undefined in all cases unless the property exists somewhere on the prototype chain.

In the last case,

console.log(user.toString()) // [object Object] ​​​​

The above result is not undefined but an actual return value. This is because the property toString() lives in an object Prototype that is connected to this object (user) somewhere through the prototype chain.

Let's check this out in Google Dev Tools.

Screenshot 2022-04-24 at 4.55.23 PM.png

Setting Prototype

The Object.setPrototypeOf() method sets the prototype (i.e., the internal [[Prototype]] property) of a specified object to another object or null.


const entity = {
    isHuman: true
};

const vishwajeet = {
    firstName: 'Vishwajeet',
    lastName: 'Raj'
};

Object.setPrototypeOf(vishwajeet, entity);

console.log(vishwajeet.firstName) // Vishwajeet
console.log(vishwajeet.isHuman) // true

In the above example, we are setting the prototype or the next inline chain object of vishwajeet to be entity.

The reason why vishwajeet.isHuman is not undefined is because we set entity as prototype of vishwajeet.

The prototype chain is only searched when the property does not exist on the object.

Viewing the relationship in chrome dev tools

image.png

for-in loop on Objects with Prototype

if you use a for..in loop to iterate over an object, any property that can be reached via its chain and is also enumerable will be enumerated. If you use the in operator to test for the existence of a property on an object, it will check the entire chain of the object (regardless of enumerability).


const person = {
    name: 'Vishwajeet',
    lastName: 'Raj'
}

const extraDetails = {
    age: 23,
    eatsApples: true
}

Object.setPrototypeOf(person, extraDetails)

let n = 0;

for (let property in person) {
    n++
}

console.log(n)  // 4

In this example here all the enumerable properties were counted.
If we were to only count the properties only belonging to the object person.
We would do it this way:

const person = {
    name: 'Vishwajeet',
    lastName: 'Raj'
}

const extraDetails = {
    age: 23,
    eatsApples: true
}

Object.setPrototypeOf(person, extraDetails)

let n = 0;

for (let property in person) {
    if(person.hasOwnProperty(property) ) {
        n++;
    }
}

console.log(n)  // 2

Prototype delegation with the new keyword

The new keyword does the following things:

  • Creates a blank, plain JavaScript object.
  • Adds a property to the new object (proto) that links to the constructor function's prototype object
  • Binds the newly created object instance as the this context (i.e. all references to this in the constructor function now refer to the object created in the first step).
  • Returns this if the function doesn't return an object.

Example 1

function Laptop(maker) {
    this.maker = maker
    this.ram = 4
}

console.log(Laptop.prototype) // Laptop {}

const myLaptop = new Laptop('Apple')

console.log(myLaptop.ram)  // 4
console.log(myLaptop.color) // undefined

console.log(myLaptop.__proto__ === Laptop.prototype) // true

Prototype objects are created when functions are declared.

In the above example: We see that the prototype object of the Laptop doesn't have any properties on it. We get color undefined because when we look up color in myLaptop object it exists neither on the myLaptop object nor on the prototype.

At this point, you may ask

what's the difference between proto and prototype?

Well, the difference between prototype and proto is that the former is a property of a class constructor, while the latter is a property of a class instance.

Example 2

function Laptop(maker) {
    this.maker = maker
    this.ram = 4
}

Laptop.prototype.ram = 8
Laptop.prototype.color = 'black'

console.log(Laptop.prototype) // Laptop { ram: 8, color: 'black' }

const myLaptop = new Laptop('Apple')

console.log(myLaptop.ram)  // 4
console.log(myLaptop.color) // black

console.log(myLaptop.__proto__ === Laptop.prototype) // true

Remember this?

The prototype chain is only searched when the property does not exist already on the object.

In the above example. The JS engine finds the ram property so it doesn't look up further in the prototype chain. In the case of color, it's not found on the new Laptop instance so it looks further in the prototype chain and finds it.

Understanding Constructor Property on Objects.

Every function has the prototype property even if we don’t supply it.
The default "prototype" is an object with the only property constructor that points back to the function itself.

Example 1

function func() {
    console.log('some')
}

console.log(func.prototype.constructor) // [func function itself]

const instance = new func();

console.log(instance.constructor === func) // true

In the above example, constructor in not a property that exists on instance but on the prototype of the instance.

func.prototype has only one accessible property which is constructor which will point back to the memory reference location of the function the prototype object was created with. Logging it would give you the function definition.

The constructor property will not always point to the function that created it. We can assign the prototype to a new object.

Example 2

function func() {
    console.log('some')
}
func.prototype = {} 

const instance = new func();

console.log(instance.constructor === func) // false

By doing this mutation, we have eventually set an Object as the prototype of the func function. In this case, the constructor will return the global Object constructor and the instance.constructor will be equal to Object.

Example 3

function func() {
    console.log('some')
}

func.prototype = {}

console.log(func.prototype.constructor === Object) // true

const instance = new func();

console.log(instance.constructor === Object) // true

Prototype Delegation with class Keyword

The class keyword was introduced with ES6. It's just syntactic sugar over a regular JavaScript function.

class Human {}

console.log(typeof Human) // function

Classes in js use prototypal inheritance.

However, instead of properties being copied from one class to another, which is done in classical languages, methods and properties that are written inside of a class are actually created on the prototype object of that class.

The most important difference between class- and prototype-based inheritance is that a class defines a type which can be instantiated at runtime, whereas a prototype is itself an object instance.

Example 1

class Human {
    isAlive() {
        return true
    }
}

class Doctor extends Human {
    status() {
        return this.isAlive();
    }
}

console.log(Human.prototype.isAlive()) // true


const doc = new Doctor();

console.log(Object.getPrototypeOf(doc) === Doctor.prototype) // true

console.log(Object.getPrototypeOf(Doctor.prototype) === Human.prototype) // true

The extends keywords creates what looks and acts similar to a classical parent-to-child relationship.

In the above example, with use of the extends keyword, Human's prototype object, not the class itself, is prototype-linked to the Doctor's prototype object. We're only able to access the isAlive function because isAlive actually lives on the Human's prototype object, not on the class itself.

Visualization in Chrome Dev Tools

image.png

#javascript #prototypal_inheritance #thw-web-apps

Did you find this article valuable?

Support Vishwajeet Raj's Blog by becoming a sponsor. Any amount is appreciated!