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.
Table of contents
- Prerequisites
- What is a prototype?
- What is inheritance?
- Object.prototype
- A close look into prototypes
- The new Operator
- How does JavaScript execute the code?
- Establishing and mutating prototype chains.
- Using Object.create()
- Using Object.setPrototytpeOf()
- Using the __proto__ accessor.
- Using constructor functions
- Declaring Constructor Functions
- Using ES6 classes
- Declaring classes
- Class elements
- Distinction between private, public, and static class elements.
- Public class elements
- Private class elements
- Static class elements
- The constructor
- Methods
- Getters
- Setters
- Fields
- Conclusion
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 theinstance
object created, (which is actually what a prototype is). The same way parents cannot inherit traits from their children, is the same wayPrototypeTest()
cannot inherit methods and properties frominstance
. Ratherinstance
inherits fromprototypeTest()
.
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.