DevelopmentJavaScript | 7 Min Read
JavaScript Fundamentals: Prototypal Inheritance
Prototypal Inheritance is one of the more confusing topics in JavaScript so in this blog post I brake it down and explain it.
What is it?
Prototypal inheritance is the process an object uses in JavaScript to lookup properties and methods from it's prototype should it not be found in the current object.
At first the object looks for the property within itself, if it's not found it looks up to it's prototype for it, if it's not found there it looks in the next prototype and this process continues until either method / property is found or it reaches the end of the prototype chain which is the value null.
Example
1const shop = {2 location: "United Kingdom",3 currency: "GBP",4};5
6const foodShop = {7 name: "Food Shop",8};9
10console.log(foodShop); // Object {name: "Food Shop" }11console.log(foodShop.location); // undefined12
13foodShop.__proto__ = shop; // Setting the prototype of foodShop to the shop object. <--- Don't use __proto__ in real life, see end of article for why.14
15console.log(foodShop); // Logs the foodShop object as before but with the shop as it's prototype.16console.log(foodShop.location); // "United Kingdom"
jsxSo, what's going on here? Let's start by breaking down what happened above.
- First we log the
`foodShop`
object and it logs fine. - Then we try to log
`foodShop.location`
and it returns undefined as`.location`
doesn't exist in that object - We set the prototype of
`foodShop`
to be the`shop`
object - We re-log the
`foodShop`
object and it displays as normal but with the prototype as the`shop`
object. - We try access
`foodShop.location`
again and it returns the value.
Now the reason the above works fine after we set the prototype is because of prototypal inheritance.
The Chain
While it sounds fancy in fact it's quite logical, when we try to access a property/method of an object at first it looks within itself, if it can find it great! It returns the code or returns the value. But, if it can't find the requested property/method it then looks up the chain to the next link in the prototype chain, in this case the `shop`
object. This continues on until we find what we're after or there is no more prototypes to access and we return null.
By default an object doesn't have a custom prototype chain like shown above, by default an object literal prototype chain would look like:
1null2^3Object.prototype4^5Our Object
The Object.prototype is the prototype shared by all objects, this contains the built in methods like `.keys()`
, .`.toString()`
and all the other methods we're use to using on Objects.
We're the magic happens so to speak is where we get involved and assign a custom prototype to an object, as one object can only inherit from one prototype this is what happens where we assign a new prototype like above:
1null2^3Object.prototype4^5shop6^7foodShop
This is why in our example, when we accessed `.location`
it returned the value from within the `shop`
object. It first looked within the `foodShop`
object and couldn't find it so it went looking in the next chain in the prototype, found the value and returned it.
It's not limited to just one extra link either, we could add more links to the prototype chain like so:
1null2^3Object.prototype4^5shop6^7foodShop8^9isle10^11item
So, if we access the `item`
object and it can't find the property we requested, it'll continue looking up the chain until it finds it or it hits null.
Read only
The prototype and by extension the prototype chain is only used for reading properties, when it comes to writing/deleting properties & methods they are added/removed to the object it was called on regardless if one is the in the prototype or not. For example:
1const car = {2 drive() {3 console.log("broom broom"); // Ignored by fastCar object after we write the method to it.4 },5};6
7const fastCar = {8 __proto__: car,9};10
11fastCar.drive = function () {12 console.log("vrooooomm");13};14
15fastCar.drive(); // vrooooomm
jsxBecause we are writing a new property/method to the `fastCar`
object, it doesn't lookup the prototype chain to check for existing ones, it just writes the new one onto the object. In the future now, the `fastCar`
object will always use the `drive`
method that we added to it and not the one on `car`
.
Accessor properties are the exception
Accessor properties are the exception to the read only rule because an assignment is handled by a setter function so writing to the property is the same as calling a function. For example, let's take the example from earlier:
1const shop = {2 location: "United Kingdom",3 currency: "GBP",4 age: 0,5 set age(val) {6 this.age = val;7 },8 get age() {9 return age;10 },11};12
13const foodShop = {14 __proto__: shop,15 name: "Food Shop",16};17
18console.log(foodShop.age); // 019
20// Triggering the setter found in the chain21foodShop.age = 10;22
23console.log(foodShop.age); // 10 <- State of foodShop has been changed.24console.log(shop.age); // 0 <- State of shop has been preserved.
jsxUnlike the example earlier showing how properties/methods are added even if the prototype has a matching value, when it comes to getters and setters we use the function from the chain but the value stored is tied to the calling object.
So, in our example `foodShop`
called the getter first but because we hadn't called the setter it had no value to pick up so fell back to the one within the `shop`
object. However, once we called the setter and added an `age`
value to the `foodShop`
object, when we ran the getter again this is the value it found and returned, so we finished with 2 different values on the object itself and the object up the chain.
"this"
The "this" keyword can be confusing at the best of times but to keep things easy to remember when it comes to the prototype, just remember:
No matter where the method is found whether it be in the object itself or within the prototype chain. When calling a method the "this" keyword always refers to the object before the dot.
So, in the example shown above when we called `foodShop.age = 10`
, the "this" value was equal to `foodShop`
and not `shop`
.
This is helpful because it means that if many objects share one prototype object they will share the methods but not the state.
1const car = {2 drive() {3 this.isDriving = true;4 },5};6
7const car1 = {8 __proto__: car,9};10
11const car2 = {12 __proto__: car,13};14
15// Modify the state within car116car1.drive();17
18console.log(car1.isDriving); // true19console.log(car2.isDriving); // undefined
jsxWhen we use "this" to write data, it is stored within the object that called it and not the one where the method was found in the chain. Therefore, we can have many objects sharing one prototype, all sharing it's methods but with their own state.
Looping
If you want to return the properties of an object, you can do this via `Object.keys()`
but this will ignore any inherited properties, if you want these to be included you would be better served using a `for..in`
loop like so:
1const shop = {2 location: "United Kingdom",3 currency: "GBP",4 age: 0,5};6
7const foodShop = {8 __proto__: shop,9 name: "Food Shop",10};11
12// Object.keys only returns the properties directly on that object13console.log(Object.keys(foodShop)); // ['name']14
15// for..in loops over it's own properties as well inherited.16for (let prop in foodShop) console.log(prop); // name, location, currency, age
jsxWhen we use the `for..in`
loop we loop over the properties on the actual object first before then working up the prototype chain.
If you would like to check if a property is built into that object while looping through it, you can use `Object.prototype.hasOwnProperty()`
, this returns a boolean value representing if the property passed is directly on the object or inherited from the prototype. For example:
1const shop = {2 location: "United Kingdom",3 currency: "GBP",4 age: 0,5};6
7const foodShop = {8 __proto__: shop,9 name: "Food Shop",10};11
12console.log(foodShop.hasOwnProperty("name")); // true13console.log(foodShop.hasOwnProperty("location")); // false
jsxThe interesting thing is that in itself `Object.prototype.hasOwnProperty()`
is inherited into any object as it's part of the Object.prototype but yet when we loop through the properties of an object, it along with the other properties on the Object.prototype do not show, why is this?
This behaviour is due to the prototype properties having the flag `enumerable:false`
and because a `for..in`
loop will only list enumerable properties it skips over the ones inherited from the Object.prototype but not ones inherited from prototype that we have set.
The use of proto
I have used the syntax `__proto__`
a fair amount throughout this post but this is purely for demonstration purposes as the code is easy to read and understand. The use of `__proto__`
is now deprecated and no longer recommended. Instead we should be using Object.getPrototypeOf/Reflect.getPrototypeOf and Object.setPrototypeOf/Reflect.setPrototypeOf.
If you're interested in reading more about the above then I recommended checking out the page on proto on MDN here.