Getting Started with Prototypal Inheritance in JavaScript.

Getting Started with Prototypal Inheritance in JavaScript.

A deep dive into JavaScript prototype chain and how the various ways to create and change an object prototype.

Just as parents pass on specific characteristics, behaviors, or traits to their children, the same can be said about JavaScript’s object-oriented nature. With JavaScript prototypes (which act as parents in this case), objects can inherit properties and methods from other objects and create a prototype chain (relationship).

You might be asking why you need to understand prototypal inheritance. Well, for one, you need to understand this concept to write efficient and well-organized code. Prototypal inheritance is how JavaScript handles code reuse and the relationships between objects. So, writing reusable code is very essential in the world of programming.

In this article, we will take a deep dive into JavaScript constructor functions, JavaScript classes, inheritance in JavaScript, and the various ways to create and mutate prototypes in JavaScript.

Prerequisites

  • Basic JavaScript Knowledge.

  • Object in JavaScript (understand how to create objects and access properties).

What is a prototype?

Every object in JavaScript has a link to another object called its prototype. A prototype is an object from which other objects inherit their properties and methods.

What is an object?

An object is a self-contained entity that contains properties and methods. A property is a key-value pair that holds different kinds of data, such as numbers, booleans, or strings. While methods are properties in an object whose value is a function. You can think of the properties and methods as traits passed from a parent to a child. Consider the example below:

const propertyObject = {
    nameProperty: "WD's blog", // A property
    numberProperty: 7, // A property

    method() {  // A method
        console.log(this.nameProperty); 
    },
};

What is inheritance?

Inheritance is the process of establishing a relationship between objects, which allows for the transfer of properties from one object (the prototype) to another.

At this point, it's important to note that when the term object is mentioned in the context of inheritance in JavaScript, we are not just referring to regular objects for example:

const regularObject = {}

Objects in this context also refer to arrays[] and functions. Yes in Javascript, functions and arrays are also objects.

As mentioned before, every object in JavaScript has a link to another object called its prototype. For example, when you access a property in an object, JavaScript first checks if that property is available in that object. If it isn't, it then checks its prototype for the property. This process repeats until the object is found, or null is returned if it isn't.

Object.prototype

The object at the top of the prototype chain is Object.prototype. It's the supertype of all objects in JavaScript. Meaning, every object in JavaScript, be it a function, array, or even Object{} themselves, inherits properties and methods from Object.prototype. However, built-in objects like arrays[], functions and regular expressions have prototypes other than Object.prototype. While objects inherit methods and properties from Object.prototype, also maintain access to methods and properties from their own prototypes.

When you first create an object in JavaScript, the object automatically inherits properties and methods from Object.prototype. Some of the methods in Object.prototype are:

  • toString()

  • valueOf()

  • hasOwnProperty()

To view the methods and properties in Object.prototype, open your console and log Object.prototype.

console.log(Object.prototype);

This should output the following:

Above is a list of properties and methods available in the Object.prototype object, they vary between browsers. I call it an object because Object.prototype is an object itself. The core purpose of inheritance in JavaScript is to be able to create and reuse properties and methods.

Now, let's create an object and check its prototype using getPrototypeOf the method. The getPrototypeOf method is used to get the prototype of an object.

// Create a new object
const myObject = {};

// Use Object.getPrototypeOf() to get the prototype of myObject
const myObjectPrototype = Object.getPrototypeOf(myObject);

// Checking for the object's prototype
console.log(myObjectPrototype === Object.prototype); // logs: True

The code above logs "true" because we compared Object.prototype to an actual object. However, comparing Object.prototype against arrays or any other object aside from traditional JavaScript objects will log "False" because arrays and functions have prototypes of their own, which are:

  • Array.prototype

  • Function.prototype

Let's look at the difference.

Comparing Object.prototype against arrays ([]).

// Create a new array
const myArray = [];
const myArrayPrototype = Object.getPrototypeOf(myArray);

// Checking for the array's prototype
console.log(myArrayPrototype === Object.prototype); // logs: False

Comparing Object.prototype against functions.

// Create a new function
function myFunction() {}
const myFunctionPrototype = Object.getPrototypeOf(myFunction);

// Check for the function's prototype
console.log(myFunctionPrototype === Object.prototype); // logs: False

As you can see, Object.prototype is not the prototype of arrays and functions. You might be asking how Object.prototype is the prototype of all objects. It's is referred to as the prototype of all objects because all objects inherit the properties and methods in Object.prototype at creation. For example, functions and arrays can use the toString() method in Object.protoype. This makes Object.prototype The prototype of all objects in JavaScript.

Now, let's compare an array and a function to their prototype.

ARRAY:

// Create a new array
const myArray = [];
const myArrayPrototype = Object.getPrototypeOf(myArray);

// Checking for the array's prototype
console.log(myArrayPrototype === Array.prototype); // logs: True

FUNCTION:

// Create a new function
function myFunction() {}
const myFunctionPrototype = Object.getPrototypeOf(myFunction);

// Checking for the function's prototype
console.log(myFunctionPrototype === Function.prototype); // logs: True

A close look into prototypes

All functions have a unique property called prototype. It is used to define or create properties and methods that will be shared among the instances of an object.

Below is how we can add properties and methods to an object's prototype.

function prototypeTest() {};
// adding a property to the prototype of prototypeTest() 
prototypeTest.prototype.addedProperty = 'This is an added property';
console.log(prototypeTest.prototype); // logs:

This should output the following:

The new Operator

One way to establish inheritance in JavaScript is through the new operator. It's used to create and initialize instances of objects; it can also be used with constructor functions, which are functions designed to configure objects. To use the new operator, call a function as you would normally call it and add a new prefix.

This will create an instance of the object, more like giving birth to a new object. An instance is an object created from the template of a constructor function. More on constructor functions later.

The code below shows how we can create instances of objects with the new operator.

function prototypeTest() {}
prototypeTest.prototype.addedProperty1 = 'this is the first instance string';
const instance = new prototypeTest(); //create an instance with the "new" operator
instance.addedProperty2 = 'this is the second instance string'; // adding a property the instance.
console.log(instance.a);

Note that properties created in instance-specific properties can not be accessed in the prototype. I.e. they are not shareble, but properties or methods created in the prototype are shared with every instance of the prototype.

To further clarify the statement, see prototypeTest() as a parent to the instance object created, (which is actually what a prototype is). The same way parents cannot inherit traits from their children, is the same way PrototypeTest() cannot inherit methods and properties from instance. Rather instance inherits from prototypeTest() .

Using the code above, let's create an instance and add a property to it. The properties created from an instance are called "instance-specific" properties.

function prototypeTest() {}
prototypeTest.prototype.addedProperty1 = 'this is the first instance string';
const instance = new prototypeTest(); 
instance.addedProperty2 = 'this is the second instance string'; // instance-specific property
console.log(prototypeTest.addedProperty2); // logs: "undefined".

How does JavaScript execute the code?

Before we examine how JavaScript executes the code above, let's talk about [[Prototype]].

[[Prototype]] is a JavaScript hidden object property that holds a reference to an object’s prototype. In JavaScript, when you need to search for something on an object (such as a property or a method), it first checks if the object itself has that particular property or method. If it does not find it, it then follows the hidden link to another object. This process is like going up the family tree to find answers until the desired thing is found.

So [[Prototype]] is a hidden link JavaScript uses to link an object to its prototype. Chrome uses [[Prototype]] while Firefox uses <prototype>.

Now, Looking at the code above, you will see that the [[Prototype]] of instance is prototypeTest() . When the above code is executed, it follows a process. Let's look at how JavaScript runs the above code.

By convention, when you access a property of instance, JavaScript engine looks to see if that property is present in instance. If the property is not found, the engine then checks instance.[[Prototype]] that is, the prototype (parent) of the instance created which is ProtoypeTest(). If found, the property is then used.

Otherwise, the JavaScript engine checks instance.[[Prototype]].[[Prototype]] a.k.a prototypeTest.prototype, that is, the prototype of prototypeTest(), which is Object.prototype. If the property isn't found in Object.prototype, the engine returns undefined.

A visual representation of the prototype chain would be;

prototypeTest ==> instance ==> Object.prototype ==> undefined.

Establishing and mutating prototype chains.

So far, we have looked at what a prototype is and how inheritance is achieved in JavaScript. Let's take a deeper look at how we can create and mutate (change) a prototype chain.

Using Object.create()

As the name goes, Object.create creates a new object and sets an existing object as the prototype of the new object created. It directly sets the [[Prototype]] of an object upon creation. Object.create is supported by most modern browsers; however, IE8 and older versions do not support it.

Consider the following code:

// The prototype object
const grandPrototype = {
    property: 'Inherited property',
};

// Sub properties
const prop2 = Object.create(grandPrototype); // making grandPrototype the prototype to prop2
const prop3 = Object.create(prop2); // making prop2 the prototype of prop3
console.log(prop3.property); //logs: "Inherited property"

Using Object.create, we made grandPrototype the prototype of prop2 and prop2 the prototype of prop3. If you noticed, we accessed the property in grandPrototype from prop3 . This was possible because prop2 inherits from grandPrototype and we made prop2 the prototype of prop3. So whatever prop2 inherits, prop3 inherits as well.

This is what the prototype chain looks like now; 👇

prop3 --> prop2 --> grandPrototype --> Object.prototype --> null

Using Object.setPrototytpeOf()

We can also assign a prototype with Object.setPrototypeOf(). The Object.setPrototypeOf() method does not just set a prototype, it can also mutate an object's prototype. It takes two values, the first one is the "object" and the second is the prototype.

SYNTAX:

Object.setPrototypeOf(object, prototype);

Assigning an object's prototype with Object.setPrototypeOf().

const firstObject = {
    firstProperty: "This is the first object's property",
};

const secondObject = {
    secondProperty: "This is the second object's property",
};

Object.setPrototypeOf(secondObject, firstObject); // making firstObject the prototype of the secondObject.
//checking secondObject prototype using isPrototypeOf()
console.log(firstObject.isPrototypeOf(secondObject)); // logs: true

Mutating an object's prototype with Object.setPrototypeOf().

We made firstObject the prototype of secondObject before, now let's make secondObject the prototype of firstObject .

const firstObject = {
    firstProperty: "This is the first object's property",
};

const secondObject = {
    secondProperty: "This is the second object's property",
};

Object.setPrototypeOf(firstObject, secondObject); // making the secondObject the prototype of firstObject.
console.log(secondObject.isPrototypeOf(firstObject)); // logs: true

Looking at the examples above, we created two objects and used Object.setPrototypeOf() to assign and mutate their prototypes. Additionally, we used isPrototypeOf() methods to verify their prototype relationships which returned true.

Using the __proto__ accessor.

The __proto__ accessor is a non-standard JavaScript property that is used to explicitly manipulate an object's prototype chain. However, it should be carefully used as it's a non-standard method and can lead to compatibility issues. This is how it can be used:

"Assigning/mutating" prototype using __proto__ .

// using __protot__ to assign a prototype
const childProto = {};
const parentProto = { x: 20 };

childProto.__proto__ = parentProto;

console.log(parentProto.isPrototypeOf(childProto)); // logs: true

In the example above, we assigned parentProto as the prototype of childProto using childProto.__proto__.

Mutating the prototype using __proto__ .

// using __proto__ to mutate an Object's prototpye.
const grandparentProto = { y: 40 };
const parentProto = { x: 30 };
const childProto = {};

childProto.__proto__ = parentProto;

console.log(childProto.x); // logs: 30

// mutating childProto prototype to grandparentProto
childProto.__proto__ = grandParentProto;

console.log(childProto.y); // logs: 40
console.log(childProto.x); // logs: undefined

In this example, we first assigned parentProto as the prototype of childProto using childProto.__proto__, we then tried to access the x value in parentProto from childProto which logged 30. We then assigned grandParentProto as the prototype of childProto using childProto.__proto__ = grandParentProto. Now, childProto can access the y value in grandPatentProto. However, undefined was logged when tried to access the x value in parentProto because unlike Object.setPrototypeOf(), __proto__ permanently mutates an object's prototype.

Again, using __proto__ is non-standard instead, use Object.setPrototypeOf() method. Note that the object keyword is case-sensitive, so you should always name the first letter with an uppercase.

Using constructor functions

Constructor functions are unique functions that are used to create and initialize objects. They are used to set up the initial state of an object. They also serve as a template for creating instances of objects.

Why constructor functions?

Let's say you want to create an object and you don't want to create each car from scratch because they have the same basic parts . That's where constructor functions come in. Constructor functions will allow to use the same basic parts for each object created.

Without constructor functions, you will need to manually create different objects with the same properties, leading to code duplication and more memory use, resulting in a slow program execution.

Let's demonstrate this with a "person object".

const person1 = {
    name: 'Wisdom',
    age: 22,
    nationality: 'Nigerian',
};

const person2 = {
    name: 'Seven',
    age: 22,
    nationality: 'Nigerian',
};

As you can see, we redefined the "name", "age", and "nationality" properties on two different objects, which is not a good practice.

Declaring Constructor Functions

Constructor functions are typically named with an initial uppercase letter to create a distinction between them and regular functions.

function Box(value) {  //the 'B' in 'box' starts with an uppercase 
    this.value = value;
};

In this context, the this operator refers to the instance of the Box object being created. There is more to this which is beyond the scope of this article, but we will do fine with the definition above.

Let's refactor the "person object" above using a constructor function without duplication the person object.

function Person(name, age, nationality) {
    this.name = name;
    this.age = age;
    this.nationality = nationality;
}

const person1 = new Person('Wisdom', 22, 'Nigerian');
const person2 = new Person('Seven', 22, 'Nigerian');

console.log(person1.name); // logs: 'Wisdom'
console.log(person2.name); // logs: 'Seven'

Using ES6 classes

What we have done so far can be more structured and organized with JavaScript classes. Classes in JavaScript are special functions that determine an object's structure. They serve as a template for creating objects. They do not add to the functionality of constructor functions, however, they act as "Syntactical sugar" for constructor functions. That is, they make constructor functions more organized and structured by making them more readable. JavaScript classes were introduced in ES6 to make object-oriented programming more structured.

Declaring classes

To declare a class, write class followed by the class name. JavaScript classes are named with an initial uppercase letter.

// a class declaration
class ClassName {}

Like traditional functions, you can define classes either as an expression or a declaration.

// a class expression
const classExpression = class ClassName {};

// a class declaration
class ClassName {}

Class elements

Class elements are the different parts that make up a class declaration. They can be private, public, or static. They are as follows:

  • constructor

  • method

  • getter

  • setter

  • field

Before we talk about what private, public, and static elements are, note that all of the elements listed above except constructors can be private, public, or static.

Distinction between private, public, and static class elements.

Public class elements

Public elements are elements which can be instantiated and used outside of a class declaration.

Private class elements

Private elements are elements that cannot be instantiated and used outside of a class declaration. Private class elements are created with a # (pronounced "hash") prefix. They are not shared among instances created.

Why private elements?

Private elements have several purposes, like encapsulating implementation-specific information. They generally help store and regulate security-related operations and how data is accessed within a class.

Static class elements

In addition to the state of an element, a class element can also be a static element. A static element cannot be accessed from an instance, but it can be called from the class itself. Static class elements are created with a static prefix. Static class elements also have their private counterparts.

  • static method

  • static getter

  • static setter

  • static field

Why Static class elements?

Static properties are created for various reasons, to mention a few. You create a static element in a class declaration when shared properties and methods do not require instance-specific data.

We have seen what private, public, and static elements are, let's take a closer look at the elements themselves to further clarify their behavior.

The constructor

The constructor method is a special method used in the creation and initialization of an object within a class. It initializes the instance properties of the class, it holds parameters. Constructors in classes should not be confused with constructor functions. They serve similar purposes but differ in syntax and how they work. The constructor can be implicitly or explicitly defined, implicit constructors are declared without the constructor keyword while explicit constructors are declared with the contructor keyword in the class body.

// Explicit declaration
class className {
    constructor(parameter1, paremeter2) { // explicit constructor
        this.parameter1 = parameter1; // instance property
        this.paremeter2 = paremeter2; // instance property
    }
}
// Implicit declaration
class className {}
// the contructor is not defined here.

Methods

As defined before, a method is a property in an object whose value is a function. In this case, it's a property in a class whose value is a function.

Private Method

class className {
    constructor() {}

    #privateMethod() {  // private method
        console.log('This is a private method');
    }
}

const newClass = new className();
console.log(newClass.privateMethod()); // this will throw an error

Private methods cannot be accessed from an instance; they are only accessed and used within a class declaration. Hence, the error.

Static Method

class className {
    constructor() {}

    static staticMethod() { // static method
        console.log('This is static method');
    }
}

const newClass = new className();
console.log(newClass.staticMethod()); // this will throw an error
console.log(className.staticMethod()); // logs: "This is a statci method"

Private static method

class className {
    constructor() {}

    static #privateStaticMethod() { //  private static method
        console.log('This is a private static method');
    }
}

const newClass = new className();
console.log(newClass.privateStaticMethod());
console.log(className.privateStaticMethod());
// both logs will throw an error.

Getters

Getters are unique functions that are used to encapsulate computed data. Computed data are information given on the fly when asked for, a get accessor cannot have parameters. To define a getter, prefix a get keyword and then the method name with a pair of parenthesis "()", similar to declaring regular methods. However, note that getters cannot have parameters.

To further explain getters, imagine getters as a door. When you want to know what is behind the door, you don't open the door. Instead, you ask the door what its content is, and it tells you.

class squareNumbers {
    constructor(value) {
        this.value = value;
    }

    get number() {
        return this.value * this.value; 
    }
}
const myNumber = new squareNumbers(4);
console.log(myNumber.number); //logs: 16

Private getters

class PrivateGetter {
    constructor(value) {
        this.value = value;
    }

    get #number() { // a private getter method
        return this.value ** 2;
    }
}
const myNumber = new PrivateGetter(4);
console.log(myNumber.number); // logs: undefined 
console.log(PrivateGetter.number);

Static getters

class GetPi {
    static get pi() { // static getter
        return 3.14159;
    }
}
console.log(GetPi.pi); // logs: 3.14159
//creating an instance of GetPi
const newPi = new GetPi(); 
// accessing the pi() method from the instance
console.log(newPi.pi); // logs: undefined;

Private static getters

class GetPi {
    static get #pi() { // private static getter
        return 3.14159;
    }
}

console.log(GetPi.pi); // logs: undefined
//creating an instance of GetPi
const newPi = new GetPi();
// accessing the pi() method from the instance
console.log(newPi.pi); // logs: undefined;

Setters

Setters are unique methods used to assign and control the value of an object's property in a controlled way. They are paired with getters to define customer behavior when a property value is changed.

While getters encapsulate computed data and execute when called, setters can mutate or control these properties' values if the conditions are specified.

class Person {
    constructor(firstName, lastName) {
        this._firstName = firstName;
        this._lastName = lastName;
    }

    set firstName(value) {
        this._firstName = value;
    }

    set lastName(value) {
        this._lastName = value;
    }

    get fullName() {
        return `${this._firstName} ${this._lastName}`;
    }
}

const person = new Person('John', 'Doe');

// changing the names values
person.firstName = 'Just';
person.lastName = 'Wisdom';

console.log(person.fullName); // logs: Just Wisdom

Now, what we just did was define a "person class" with a constructor that takes _firstName and _lastName as parameters. Two setters, firstName and _lastName are defined within the class which allows you to change their values respectively.

A getter that concatenates _firstName and _lastName. We then created an instance of the person class with an initial value of "John" and "Doe", which was changed to "Just" and "Wisdom". The getter was then used to access the fullName method we logged to the console.

The underscore prefix in the names indicates that a property should be treated as private and should not be accessed directly. Any property with the # prefix is enforced as private by JavaScript itself, however, properties with the _ prefix are not enforced as private in JavaScript. Instances of that class can still access and modify properties with _ prefix outside the class body.

Private setters

class PrivateClass { 
    get #privateProperty() {  // private setter
        // Compute and return the value for the private property.
        // return the computed value;
    }
}

// cannot be accessed or used outside of PrivateClass

Static setters

class StaticClass {
    static get privateProperty() { // static setter
        // Compute and return the value for the static property.
        // return the computed value;
    }
}
// the static get property can only be access from StaticClass

Private static setter

class PrivateStaticClass {
    static get #privateProperty() { // private static setter
        // Compute and return the value for the static property.
        // return the computed value;
    }
}
// cannot be accessed or used outside of PrivateStaticClass

Fields

Fields are simply class variables or properties that can store different data types (like, numerical data, boolean), etc.

Private field

class Field {
    #privateField = 2; // private field
    constructor(value) {
        this.#privateField = value;
    }
}

Static field

class Field {
    static staticField = 2; // static field
    constructor() {}
}

Private static field

class Field {
    static #staticField = 2; // Private static field
    constructor() {}
}

Conclusion

We have covered a lot on prototypal inheritance and as a final note, I want you to view prototypal inheritance in this light, everything is either classified as an object’s instance or a constructor function. The prototype property, which works with the new operator, creates instances of an object when called. When an object is created, JavaScript copies its reference to the prototype object which is stored in [[Prototype]] .

For example, when you create an instance const newObject = new Person(), JavaScript sets newObject.[[Prototype]] = Person.prototype. When you try to access the instance property, JavaScript first checks if the property is present in the instance created. If not, if checks Person.[[Prototype]] which is the prototype of the instance newObject, if found, it's used.

Otherwise, it checks [[Prototype]].[[Prototype]] i.e. the prototype of the Person() object, which is Object.prototype . If the prototype is still not present, then null or undefined is logged to the console.

A big shout-out to you for reading this far. Thanks for reading, and I hope to catch you in my future articles. Do well to drop your comments, and if you notice a mistake or you have something to add, don't hesitate to drop those as well.