Understanding the two faces of inheritance in JavaScript — syntax sugar vs raw mechanics, and which one you should use today.
Introduction
If you’ve been coding in JavaScript for a while, you’ve probably seen both styles:
// Prototype-based
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name + " makes a sound.");
};
// ES6 Class
class Dog extends Animal {
speak() {
console.log(this.name + " barks.");
}
}
So… which is better? The “classic” prototype-based inheritance or the newer ES6 class syntax? The truth: they’re the same engine under the hood. The difference is mostly developer experience, readability, and safety.
This article dives into:
- How prototypes and classes really work.
- The pros and cons of each.
- Real-world use cases (React, Node, libraries).
- Performance considerations.
- A clear answer: which one wins in modern code.
1. JavaScript Inheritance Recap
- Every object has a hidden
[[Prototype]]
(its parent object). - Functions have a
.prototype
property, which becomes the[[Prototype]]
of objects created withnew
. - When you call a property/method on an object, JS looks up the prototype chain until it finds it (or hits
null
).
Classes? Just a cleaner way to wire up these links.
2. Prototypes — The Raw Mechanism
Before ES6, inheritance was done with constructor functions and prototypes.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name + " makes a sound.");
};
function Dog(name) {
Animal.call(this, name); // inherit props
}
Dog.prototype = Object.create(Animal.prototype); // inherit methods
Dog.prototype.constructor = Dog; // fix constructor reference
Dog.prototype.speak = function() {
console.log(this.name + " barks.");
};
const rex = new Dog("Rex");
rex.speak(); // Rex barks.
Pros:
- Explicit: you see exactly how prototypes are wired.
- Flexible: you can modify prototypes at runtime.
Cons:
- Verbose (boilerplate
Object.create
,constructor
fix). - Easy to make mistakes (forgetting
.call(this)
, messing up chains). - Harder for newcomers to read.
3. ES6 Classes — The Sugar Layer
ES6 introduced class
as syntactic sugar:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + " makes a sound.");
}
}
class Dog extends Animal {
speak() {
console.log(this.name + " barks.");
}
}
const rex = new Dog("Rex");
rex.speak(); // Rex barks.
Under the hood:
class Dog extends Animal
setsDog.prototype.__proto__ = Animal.prototype
.constructor
is just a function.methods
go on.prototype
.
Pros:
- Cleaner, more readable.
- Built-in
extends
andsuper
. - Consistent semantics across JS engines.
- Familiar to devs from Java, C#, etc.
Cons:
- Still “just sugar” — you need to know prototypes to debug deeply.
- Less flexible than raw prototypes (e.g., dynamic modification discouraged).
- Can create an illusion that JS has “classical OOP” (it doesn’t).
4. Comparing Features

5. Real-World Use Cases
Frameworks & Libraries
- React components:
Early React usedcreateClass
(prototype-based). Modern React uses class components (extends React.Component
) and, even more, functional components. - Node.js Core:
Many modules (e.g.,EventEmitter
) are built with prototypes. ES6 classes wrap or extend them in userland.
Prototypal Inheritance as a Feature
Sometimes, raw Object.create
inheritance is simpler:
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.eats); // true
No classes, no constructors — just objects linked together.
6. Performance Considerations
Modern JS engines (V8, SpiderMonkey, etc.) optimize both classes and prototypes. The performance difference is negligible for 99% of apps.
Where it matters:
- Hot loops with object creation.
- Libraries defining thousands of tiny objects.
👉 In practice: choose readability, not micro-optimizations.
7. Common Gotchas
this
Binding
class Dog {
speak() { console.log(this.name); }
}
const f = new Dog("Rex").speak;
f(); // ❌ undefined (lost `this`)
Fix with .bind
or arrow functions.
Static Methods
class Animal {
static create(name) {
return new Animal(name);
}
}
const a = Animal.create("Zoe"); // works
Prototype Pollution
Messing with built-in prototypes (Array.prototype
, Object.prototype
) is dangerous — it affects all objects.
8. So… Who Wins?
- Use ES6 Classes for:
- Readable, structured inheritance.
- Framework integration (React, Angular, TypeScript).
- Team projects where clarity matters.
2. Use Prototypes directly for:
- Lightweight object delegation (
Object.create
). - Polyfills and library internals.
- Advanced meta-programming where classes feel restrictive.
👉 Verdict (2025): ES6 Classes win for almost all modern code. Prototypes are still essential knowledge, but you’ll rarely write raw prototype chains outside of experiments or deep library internals.
Conclusion
JavaScript’s inheritance model is prototype-based at its core. ES6 classes don’t change that — they just give us cleaner, more familiar syntax.
Key takeaways:
- Classes = sugar, prototypes = engine.
- Learn prototypes to understand, use classes to write.
- Favor immutability, composition, and functions where possible — inheritance isn’t always the best tool.
Pro tip: If debugging weird inheritance, crack open the chain:
console.log(Object.getPrototypeOf(obj));
It’ll remind you what’s really going on.
Call to Action
Have you ever run into a bug because you didn’t understand prototypes behind classes?
💬 Share your story in the comments.
🔖 Bookmark this for your next interview prep.
👩💻 Send it to a teammate who still thinks ES6 classes replaced prototypes.
Leave a Reply