Understanding Prototypes and Prototypal Inheritance in JavaScript
A look at prototypes and how we can use them for inheritance in JavaScript.
This article is going to look at object prototypes and how we can use constructor functions to extend prototypes in new objects.
JavaScript is a prototype-based language, which means object properties and methods can be shared through objects that have the ability to be cloned and extended. This is known as prototypal inheritance.
In my previous article, Understanding objects in JavaScript, I went over how to create an object, how to access object properties and how to modify them. Now we are going to learn how prototypes can be used to extend objects.
Nearly all objects in JavaScript are instances of Object
which sit on the top of a prototype chain. This means they have an internal property called prototype
. The prototype
is a reference to another object and it gets used whenever JavaScript can’t find the property it is looking for on the current object.
We can check this if we create a new, empty object.
const newObject = {};console.log(newObject);
If we look in the console we can see the __proto__
property which exposes the internal prototype
of the object. This consists of several built-in properties and methods.
{}
__proto__:
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
To get the prototype
of this new object we use the getPrototypeOf() method.
const newObject = {};
const prototype = Object.create(newObject);console.log(Object.getPrototypeOf(prototype) === newObject); // true
Let’s look at a more detailed code example. We are going to create a Sandwich
constructor. We will then create an instance of Sandwich
and assign it to a new variable called mySandwich
.
function Sandwich(fillings = [], customer, bread) {
this.fillings = fillings;
this.customer = customer;
this.bread = bread;
this.describe = function() {
return `This sandwich is for ${this.customer} with the toppings ${this.fillings.join(', ')} on ${this.bread} bread`;
}
}const mySandwich = new Sandwich(['cheese', 'ham', 'lettuce', 'tomato', 'cucumber'], 'Gemma', 'wholemeal');console.log(mySandwich.describe());// This sandwich is for Gemma with the toppings cheese, ham, lettuce, tomato, cucumber on wholemeal bread
If we look at mySandwich
in the console we can see the describe function exists as an instance property.
mySandwich
Sandwich {fillings: Array(5), customer: "Gemma", bread: "wholemeal", describe: ƒ}
bread: "wholemeal"
customer: "Gemma"
describe: ƒ ()
fillings: (5) ["cheese", "ham", "lettuce", "tomato", "cucumber"]
__proto__: Object
Let’s create another instance of Sandwich
using some different parameters.
const mySandwichVeggie = new Sandwich(['lettuce', 'tomato', 'onion', 'pickles', 'olives', 'capscicum', 'spinach', 'carrot'], 'Sam', 'white');console.log(mySandwichVeggie.describe());// This sandwich is for Sam with the toppings lettuce, tomato, onion, pickles, olives, capscicum, spinach, carrot on white bread
What happens if we directly compare our newly created sandwich describe()
function with the original mySandwich
describe
method.
mySandwich.describe === mySandwichVeggie.describe // false
They are not the same function; we are duplicating functionality. If we create 50 sandwiches we would have 50 copies of the same function which could potentially cause performance issues and make our code hard to maintain.
To solve the duplication issue we can add this functionality to the prototype
.
Sandwich.prototype.describe = function() {
return `This sandwich is for ${this.customer} with the toppings ${this.fillings.join(', ')} on ${this.bread} bread`;
}
Now if we look at mySandwich
or mySandwichVeggie
, describe()
no longer exists as an instance property, it is, however, visible in the __proto__
.
mySandwich
Sandwich {fillings: Array(5), customer: "Gemma", bread: "wholemeal"}
bread: "wholemeal"
customer: "Gemma"
fillings: (5) ["cheese", "ham", "lettuce", "tomato", "cucumber"]
__proto__:
describe: ƒ ()
constructor: ƒ Sandwich(fillings = [], customer, bread)
__proto__: Object
This means that every time we create a new Sandwich
we now have access to the describe()
function.
JavaScript will check for a property on the instance first and if it doesn’t exist it will check the prototype
.
If we add a new instance property to the prototype
and to the instance itself we can see this in action.
function Sandwich(fillings = [], customer, bread) {
this.fillings = fillings;
this.customer = customer;
this.bread = bread;
this.price = '5.50';
}Sandwich.prototype.price = '4.00';mySandwich
Sandwich {fillings: Array(5), customer: "Gemma", bread: "wholemeal", price: "5.50"}
bread: "wholemeal"
customer: "Gemma"
fillings: (5) ["cheese", "ham", "lettuce", "tomato", "cucumber"]
price: "5.50"
__proto__: Object
The price of mySandwich
is 5.50
because JavaScript doesn’t need to go to the prototype
, the property exists on the instance.
Summary
- JavaScript is a prototype-based language
- Prototypal inheritance means object properties and methods can be shared through objects that have the ability to be cloned and extended
- Nearly all objects in JavaScript are instances of Object which sit on the top of a prototype chain which means they have an internal property called
prototype
- The
prototype
is a reference to another object and it gets used whenever JavaScript can’t find the property it is looking for on the current object - JavaScript will first check for a property on the instance then check the
prototype