Understanding JavaScript Inheritance: A Deep Dive into Prototypal and Constructor Patterns
Introduction
In our previous post, we explored how JavaScript tries to imitate classes and whether there is a better way. We discussed how JavaScript's class
keyword is essentially syntactic sugar over its prototype-based inheritance model. Today, we will dive deeper into JavaScript's inheritance mechanisms, focusing on the differences between classical inheritance and prototypal inheritance, and clarifying some common confusions.
JavaScript Inheritance
JavaScript was not designed to be an object-oriented programming (OOP) language, but it can still support OOP-style coding. Unlike classical inheritance, JavaScript uses prototypal inheritance, which can be implemented in two patterns:
- The prototypal pattern of prototypal inheritance.
- The constructor pattern of prototypal inheritance.
Unfortunately, JavaScript primarily uses the constructor pattern of prototypal inheritance. This decision was influenced by Brendan Eich's intention to make JavaScript look like Java, which uses classical inheritance. However, this has led to some confusion among developers.
What is a Class?
Classes were introduced in ECMAScript 2015 (ES6) to provide a cleaner way to follow OOP patterns. Despite this, JavaScript still follows a prototype-based inheritance model. Classes in JavaScript are syntactic sugar over this model, making it easier for developers to build software around OOP concepts and bringing similarities to other OOP languages like C++ and Java.
Before the class
Keyword
function Car(brand, model, color, price) {
this.brand = brand;
this.model = model;
this.color = color;
this.price = price;
}
const car = new Car("Marker", "Blue", "$3");
console.log(car);
After the class
Keyword
class ClassCar {
constructor(brand, model, color, price) {
this.brand = brand;
this.model = model;
this.color = color;
this.price = price;
}
drive() {
console.log('Vroom!');
}
}
Ok, it does look cleaner and less verbose, but is the class a real class or is it something else? Let's find out:
console.log(typeof ClassCar); // function
O wow, so the class actually hides something else behind itself. The MDN’s statement:
"Classes are in fact 'special functions'"
is a bit misleading. It’s more accurate to say that classes are syntax sugar for constructor functions.
So as we took a quick look under the hood and saw what is going on there, we can say that:
- A function with the
new
keyword is a constructor function. - Using the
new
keyword bindscar.__proto__
toCar.prototype
.
Prototype
The prototype allows the JavaScript engine to look up the methods and properties we are trying to call on an object. The prototypical relationships in JavaScript create a tree-like structure where the root is Object.prototype
. Thus, every object in JavaScript inherits from the root, which is Object.prototype
.
console.log(Object.getPrototypeOf({}) == Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
We were just able to call a method on an empty object. That is magic (for those who do not know of prototypical inheritance). Let me explain it. First, JavaScript, in order to connect one object with another, has to create a 'link' or a 'reference'. How does JS do it? Really simple, it actually attaches it as another property on an object. Like so:
{} // our empty object
[[Prototype]]:Object
constructor:ƒ Object()
hasOwnProperty:ƒ hasOwnProperty()
isPrototypeOf:ƒ isPrototypeOf()
propertyIsEnumerable:ƒ propertyIsEnumerable()
toLocaleString:ƒ toLocaleString()
toString:ƒ toString() // method we called
valueOf:ƒ valueOf()
__defineGetter__:ƒ __defineGetter__()
__defineSetter__:ƒ __defineSetter__()
__lookupGetter__:ƒ __lookupGetter__()
__lookupSetter__:ƒ __lookupSetter__()
__proto__ (get):ƒ __proto__()
__proto__ (set):ƒ __proto__()
Here we see that even though we did not create the property [[Prototype]]
, it was added there automatically. It is something that real class-based languages like Java or C# do not have. For JavaScript, it is like an emergency source. When we try to call a method that does not exist on an object itself, the JS engine goes up the prototype chain
and looks for it there. So in this case, the JS engine could not find it on our empty object, and instead went to a [[Prototype]]
object and found it there. And if it did not find it there, it would throw an error:
console.log(empty.toNumber()); // Uncaught TypeError: empty.toNumber is not a function
Here we come to the essence of classes in JavaScript. In regular OOP languages, a class is just a type that is instantiated at runtime. In JavaScript, however, we have an actual instance of an object attached to our object and it is called, you guessed it, a prototype.
Summary
Classes in JavaScript were added because it was thought that people from other programming languages could pick up JS quickly. In some ways, this is true, but it also added extra confusion in terms of which approach is better. People not knowing how prototypal inheritance works want to simply patch
it with classes. But as we saw earlier, a class is just a plain old function. There is nothing wrong with prototypal inheritance; it is just that the people responsible for JS development decided to go with the constructor pattern of prototypal inheritance.
In the next article, we will look at how to create objects with factory functions.
Top comments (0)