TL;DR:
JavaScript inheritance can be approached in two main ways: class-based and prototypal. Although theclass
keyword provides a familiar syntax for developers coming from OOP backgrounds, under the hood everything in JavaScript is based on prototypes. This article breaks down how inheritance works using both classes and constructors, clarifies the difference between methods and properties, explains prototype vs__proto__
, and shows how to extend behavior using different techniques. If you're building objects that share behavior, understanding this will save you from duplicated code and maintenance nightmares.
Introduction
Last week, I published an article on Arrow Functions in JavaScript, and while many developers found it helpful, I noticed that some readers were still struggling with a few fundamental concepts of the language — like context, this, and how objects behave under the hood.
That got me thinking: maybe it’s time we slow things down a bit and revisit some of JavaScript’s core mechanics. So I’ve decided to start a new series called “Do You Know How It Works?” — focused on demystifying the JS engine, one concept at a time.
I’m officially considering the arrow functions article as Episode 0, and today’s post is Episode 1. Let’s kick things off with one of the most misunderstood (and overused) concepts in JavaScript: inheritance — both class-based and prototypal.
⚠️ Before We Dive In — A Quick Heads-Up
Before we really get into the world of JavaScript, I want to share an important tip for anyone using these posts as a learning resource:
Having a basic foundation in programming logic — especially from lower-level languages like C or C++ — can make a huge difference in how well you understand what's going on under the hood in JavaScript (and other high-level languages).While you don’t need to master all of these, I highly recommend at least being familiar with concepts like:
- Variable types and scopes
- Loops and conditional structures
- Functions and parameter passing
- Function scopes
- Arrays and multidimensional data
- Pointers and references
- Linked lists, trees, graphs, and dynamic programming
- Code complexity and algorithmic thinking
- Basic OOP principles
Many of these fundamentals are abstracted away in modern languages like JavaScript, which can make it harder to see what’s really going on under the surface. That’s why having a solid foundation helps — you’ll learn faster, debug easier, and build with more confidence.
With that in mind... let’s get started! 🚀
Class-Based Inheritance
Inheritance is a concept used to avoid code duplication — in other words, to avoid having to write the same thing over and over again — and it also makes code maintenance easier. In JavaScript, this isn’t necessarily tied to classes, but let’s start with them. Suppose we want to represent some people in a system, doing it like this:
const person1 = {
speak() {
return "Speaking";
}
}
const person2 = {
speak() {
return "Speaking";
}
}
person1.speak(); // Speaking
person2.speak(); // Speaking
Wow, that was easy, right? Creating objects in JS using JSON is simple, but it has its downsides. Here, we don’t have any class defining a “template” for these objects, so if I want to create more or edit what a person does, I’d have to edit/create those people line by line. That’s still manageable here — two people, same file — but imagine a complex system with 1,000 people scattered across different files.
That’s where the concept of classes comes in.
class Person {
speak() {
return "Speaking";
}
}
const person1 = new Person();
const person2 = new Person();
Boom! We solved the long code duplication problem(although it wasn't that long in this example😅). Those familiar with the OOP (Object-Oriented Programming) concept in other languages have likely seen this syntax. But here’s where things start to change a bit.
Let’s address the second problem: editing a person when there’s an issue. First, let’s understand what person1
and person2
are:
typeof(person1); // "object"
console.log(person1); // Person {}
person1
is an object of type Person
. However, when we expand it, we don’t see the “speak” method inside it. Still, person1.speak()
works just fine. Why is that?
If we look inside the object, we find an interesting property: __proto__
. Inside that property, we have a constructor and our speak
method.
If we look into the Person class, more specifically Person.prototype
, we see something very similar. Not only are they similar — they’re the same thing.
person1.__proto__;
//{
// constructor: class Person()
// speak: function speak()
// __proto__: Object
//}
Person.prototype;
//{
// constructor: class Person()
// speak: function speak()
// __proto__: Object
//}
person1.__proto__ === Person.prototype; // true
This means we can “modify” class properties using one of the objects, and vice versa — we can modify the properties of all objects that are instances of a class by changing the class itself(which, by the way, is the best practice).
// Modifying through the object
person1.__proto__.speak = function() {
return "Speaking from the object";
}
Person.prototype.speak(); // "Speaking from the object"
// Modifying through the class (better practice)
Person.prototype.speak = function() {
return "Improved speaking";
}
person1.speak(); // "Improved speaking"
person2.speak(); // "Improved speaking"
All of this is class-based inheritance. But let’s put classes aside for now.
Prototypal Inheritance
Even though we used the class
keyword in the examples above, JavaScript works a bit differently. All inheritance is based on prototypes (__proto__
and prototype
). Even when we use classes, under the hood it’s still prototype-based. Using classes is just syntactic sugar — a helpful shortcut for managing inheritance and prototypes.
But it’s important to understand what’s happening behind the scenes. Essentially, when we declare a Person
class, JS is actually declaring a function and assigning what we created in the class to that function. So, the code below has essentially the same result as creating a class:
function Person() {}
Person.prototype.speak = function() {
return "Speaking";
}
const person1 = new Person();
person1.speak(); // Speaking
person1.__proto__;
//{
// constructor: f Person()
// speak: function speak()
// __proto__: Object
//}
This function is called the constructor. Again, those familiar with OOP will recognize the term. It’s the function that defines how an object instance is built. Another way to do this using a function is by assigning attributes and methods directly to its scope:
function Person() {
this.speak = function() {
return "Speaking";
}
}
const person1 = new Person();
person1.speak(); // Speaking
person1;
//{
// speak: function speak()
// __proto__: Object
//}
But here we have a fundamental difference. Notice that now the speak
function is directly inside person1
. What’s the difference?
Property/Attribute vs Method
When we use this
and assign the function directly inside the object’s scope, it becomes a property/attribute, not a method. More importantly, that function gets copied to every instance of the Person
object. When the function is in the prototype, it’s considered a method.
In the end, you still access the function the same way, but as mentioned, it’s copied to each instance. So if you want to change the behavior of the function, you’d need to change it on each object instance separately — back to our problem of editing 1,000 people all over the codebase.
Let’s see this in practice:
function Person() {
this.age = 12;
}
Person.prototype.speak = function() {
return "Speaking";
}
const person1 = new Person();
person1.age; // 12
person1.speak(); // Speaking
// Editing Person
Person.prototype.speak = function() {
return "Speaking more";
}
Person.age = 40;
// Accessing person1;
person1.age; // 12
person1.speak(); // "Speaking more"
Notice that the age change wasn’t applied to the object instances. More specifically, only what was in the prototype
was replicated across the object instances.
One more detail. Let’s recreate the person:
function Person() {
this.age = 12;
}
Person.age; // undefined
Person.prototype.age; // undefined
const person1 = new Person();
person1.age; // 12
Notice how the age
attribute isn’t present in either the definition of Person
or its prototype. age
is just an attribute/property that gets copied into each Person
instance.
This makes the separation between things I have (attributes/properties) and things I do much (methods) clearer. The things I have are individual, and the things I do can be shared and inherited between objects of the same type.
Here’s an example with another class, this time using constructors to create the object:
function Car(color, model, year) {
this.color = color;
this.model = model;
this.year = year;
}
Car.prototype.accelerate = function() {
return "Accelerating";
}
Car.prototype.showOff = function() {
return `Hey, check out my ${this.color} ${this.year} ${this.model}`;
}
const car1 = new Car("aztec gold", "Vista Cruiser", 1969);
const car2 = new Car("black", "Impala", 1967);
car1.accelerate(); // Accelerating
car2.accelerate(); // Accelerating
car1.showOff(); // Hey, check out my aztec gold 1969 Vista Cruiser
car2.showOff(); // Hey, check out my black 1979 Impala
Notice how both instances of Car
can accelerate
and showOff
, but each one shows its own characteristics when doing so.
Note: Attribute vs Property
In JavaScript, there’s no strict difference between the two, but in other OOP languages, class attributes are the values declared using this inside the class. Properties are the ones that have getters and setters (i.e., accessors and mutators).
Extending Classes
Now that we understand how classes work under the hood, let’s go back to class syntax. Classes can be extended so that behaviors from a parent class can be reused in child classes. Here’s a practical demo:
class Person {
speak() {
return "Speaking";
}
}
class SuperHuman extends Person {
fly() {
return "I believe I can fly";
}
}
const person1 = new Person();
person1.speak(); // Speaking
const hero1 = new SuperHuman();
hero1.speak(); // Speaking
hero1.fly(); // I believe I can fly
Notice that, even though it’s not explicitly declared, an instance of SuperHuman
can speak like a regular person, but can also fly — something a regular Person
instance cannot do.
Other Ways to Create Inheritance
Until now, to create objects with inheritance properties (__proto__
), we’ve always used the new keyword. But there are other ways to do it using pure objects and a little help from the Object class, which defines how objects are structured.
const person = {
speak() {
return "Speaking";
}
}
// Method 1
const me = Object.create(person);
person.speak(); // Speaking
me.speak(); // Speaking
// Method 2
const you = {};
Object.setPrototypeOf(you, person);
Essentially, they all do the same thing. Personally, for organizational reasons, I prefer having defined classes for objects I’ll reuse in the system — such as request bodies for an API route or the return type of one of those routes.
When to Use Inheritance
Simple: whenever you’re creating objects in the system that share behavior. Here, we used basic real-world examples to make things easier to follow. But in real scenarios, we have things like React component creation using classes:
class LoginPanel extends React.Component { ... }
We’re creating our own component that extends the basic behaviors of React components.
When using Web Components, the idea is the same — we extend base behavior of an HTML element:
class DefaultTable extends HTMLElement { ... }
Final Thoughts
Test it, create, play around with these elements and understand the context of inheritance. In the next post, we’ll dive into the difference between prototype
and __proto__
.
💬 Have any thoughts, questions, or cool uses of inheritance you've come across?
Drop a comment — I’d love to learn how you’re using this in your projects!
👉 Follow me @matheusjulidori for the next episodes of Do You Know How It Works?
Next up: __proto__
vs prototype
– what's the real difference?