JavaScript OOP: Prototypes vs Classes Explained with Examples
Master JavaScript prototypal inheritance vs ES6 classes. Learn prototypes, prototype chain, constructor functions, and when to use classes or prototypes for clean, modern JavaScript code

JavaScript Classes Are Syntactic SugarβHere's What That Really Means
Coming from Java, I was excited to see ES6 classes in JavaScript:
class Person {
constructor(name) {
this.name = name;
}
}
"Finally! JavaScript has real classes like Java!"
Then my senior developer dropped a bomb: "JavaScript classes are just syntactic sugar over prototypes. They're not 'real' classes."
Mind blown. JavaScript's OOP is fundamentally differentβit's prototypal, not classical.
Today, I'll demystify JavaScript's prototype system and show you when to use prototypes vs classes.
The Fundamental Difference
Classical Inheritance (Java, C++)
- Classes are blueprints
- Objects are instances of classes
- Inheritance is class-based
Prototypal Inheritance (JavaScript)
- Objects inherit directly from other objects
- No "true" classes (ES6 classes are syntax sugar)
- Inheritance is prototype-based
Key insight: In JavaScript, every object has a prototype (another object it inherits from).
Understanding Prototypes
Every JavaScript object has a hidden [[Prototype]] property (accessible via __proto__ or Object.getPrototypeOf()).
const person = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
// Create object that inherits from person
const student = Object.create(person);
student.name = "Bob";
student.subject = "Math";
// Prototype chain in action
student.greet(); // "Hello, I'm Bob" (inherited from person!)
console.log(student.name); // "Bob" (own property)
console.log(student.subject); // "Math" (own property)
console.log(student.greet); // [Function] (inherited from prototype)
// Check prototype
console.log(Object.getPrototypeOf(student) === person); // true
How it works:
- JavaScript looks for
greetonstudentobject - Not found β checks
student's prototype (person) - Found! Uses
person.greet thisrefers tostudent(the calling object)
Constructor Functions (Old Way)
Before ES6 classes, we used constructor functions:
// Constructor function (capitalized by convention)
function Person(name, age) {
this.name = name;
this.age = age;
}
// Add methods to prototype
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}, ${this.age} years old`);
};
Person.prototype.getInfo = function() {
return `${this.name} (${this.age})`;
};
// Create instances
const alice = new Person("Alice", 25);
const bob = new Person("Bob", 30);
alice.greet(); // "Hello, I'm Alice, 25 years old"
bob.greet(); // "Hello, I'm Bob, 30 years old"
// Methods are shared via prototype (memory efficient!)
console.log(alice.greet === bob.greet); // true (same function)
// Check prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
Why use prototype for methods?
- Memory efficiency - Methods are shared, not copied to each instance
- Dynamic updates - Add methods to all instances even after creation
// Add method after instances are created
Person.prototype.sayGoodbye = function() {
console.log(`${this.name} says goodbye!`);
};
// Both instances now have this method!
alice.sayGoodbye(); // "Alice says goodbye!"
bob.sayGoodbye(); // "Bob says goodbye!"
ES6 Classes (Modern Way)
ES6 classes provide cleaner syntax but work the same under the hood:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, I'm ${this.name}, ${this.age} years old`);
}
getInfo() {
return `${this.name} (${this.age})`;
}
}
const alice = new Person("Alice", 25);
const bob = new Person("Bob", 30);
alice.greet(); // "Hello, I'm Alice, 25 years old"
// Still uses prototypes under the hood!
console.log(typeof Person); // "function" (it's a constructor function!)
console.log(alice.greet === bob.greet); // true (shared via prototype)
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
Key point: ES6 classes are syntactic sugar over constructor functions and prototypes!
Side-by-Side Comparison
Constructor Function Approach
// Constructor function
function Animal(name) {
this.name = name;
}
// Add methods to prototype
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// Inheritance
function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}
// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Add Dog-specific methods
Dog.prototype.bark = function() {
console.log(`${this.name} says Woof!`);
};
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // "Buddy is eating" (inherited)
dog.bark(); // "Buddy says Woof!"
ES6 Class Approach
// Parent class
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
// Child class
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} says Woof!`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // "Buddy is eating" (inherited)
dog.bark(); // "Buddy says Woof!"
Same result, cleaner syntax!
The Prototype Chain
When you access a property, JavaScript:
- Checks the object itself
- If not found β checks object's prototype
- If not found β checks prototype's prototype
- Continues until reaching
null
class Animal {
eat() {
console.log("Eating...");
}
}
class Dog extends Animal {
bark() {
console.log("Woof!");
}
}
const dog = new Dog();
// Prototype chain visualization
console.log(dog.bark); // Found on Dog.prototype
console.log(dog.eat); // Found on Animal.prototype (Dog's prototype's prototype)
console.log(dog.toString); // Found on Object.prototype (top of chain)
// The chain:
// dog β Dog.prototype β Animal.prototype β Object.prototype β null
Visualizing the chain:
const dog = new Dog();
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null (end of chain)
When to Use Prototypes vs Classes
β Use ES6 Classes When:
- You want clean, readable syntax
- Coming from classical OOP backgrounds (Java, C#, Python)
- Working in modern codebases
- Need inheritance hierarchies
- Team prefers class syntax
// Clean, readable class
class UserService {
constructor(database) {
this.database = database;
}
async createUser(userData) {
return await this.database.save(userData);
}
}
β Use Prototypes/Constructor Functions When:
- Supporting older browsers (pre-ES6)
- Need dynamic prototype manipulation
- Performance is critical (tiny optimization)
- Understanding how JavaScript really works
// Dynamic prototype modification
function Plugin() {}
Plugin.prototype.version = "1.0.0";
const p1 = new Plugin();
console.log(p1.version); // "1.0.0"
// Update all instances dynamically
Plugin.prototype.version = "2.0.0";
console.log(p1.version); // "2.0.0" (updated!)
Advanced: Mixins with Prototypes
Prototypes make it easy to create mixins (mixing functionality from multiple sources):
// Mixin: abilities to add to objects
const canEat = {
eat() {
console.log(`${this.name} is eating`);
}
};
const canWalk = {
walk() {
console.log(`${this.name} is walking`);
}
};
const canSwim = {
swim() {
console.log(`${this.name} is swimming`);
}
};
// Apply mixins to class
class Animal {
constructor(name) {
this.name = name;
}
}
// Mix in abilities
Object.assign(Animal.prototype, canEat, canWalk);
class Duck extends Animal {}
// Duck gets additional ability
Object.assign(Duck.prototype, canSwim);
const duck = new Duck("Donald");
duck.eat(); // "Donald is eating" (from mixin)
duck.walk(); // "Donald is walking" (from mixin)
duck.swim(); // "Donald is swimming" (from mixin)
Real-World Example: Plugin System
Building an extensible plugin system using prototypes:
// Base Application
class Application {
constructor() {
this.plugins = [];
}
registerPlugin(plugin) {
this.plugins.push(plugin);
console.log(`β
Plugin "${plugin.name}" registered`);
}
start() {
console.log("π Application starting...");
this.plugins.forEach(plugin => plugin.init());
}
}
// Plugin base "class" (using constructor function)
function Plugin(name) {
this.name = name;
}
Plugin.prototype.init = function() {
console.log(`π¦ ${this.name} plugin initialized`);
};
// Create specific plugins
function LoggerPlugin() {
Plugin.call(this, "Logger");
}
LoggerPlugin.prototype = Object.create(Plugin.prototype);
LoggerPlugin.prototype.constructor = LoggerPlugin;
LoggerPlugin.prototype.init = function() {
Plugin.prototype.init.call(this);
console.log(" π Logger ready");
};
LoggerPlugin.prototype.log = function(message) {
console.log(`[LOG] ${message}`);
};
function CachePlugin() {
Plugin.call(this, "Cache");
}
CachePlugin.prototype = Object.create(Plugin.prototype);
CachePlugin.prototype.constructor = CachePlugin;
CachePlugin.prototype.init = function() {
Plugin.prototype.init.call(this);
console.log(" πΎ Cache ready");
};
// Use the system
const app = new Application();
app.registerPlugin(new LoggerPlugin());
app.registerPlugin(new CachePlugin());
app.start();
Common Pitfalls
Pitfall 1: Modifying Object.prototype
// β BAD: Never do this!
Object.prototype.myMethod = function() {
return "bad idea";
};
// Now EVERY object has this method
const obj = {};
console.log(obj.myMethod()); // "bad idea"
// This breaks libraries and causes conflicts!
Pitfall 2: Arrow Functions in Prototypes
// β BAD: Arrow functions don't have own 'this'
function Person(name) {
this.name = name;
}
Person.prototype.greet = () => {
console.log(`Hello, I'm ${this.name}`); // 'this' is undefined!
};
// β
GOOD: Use regular functions
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
Pitfall 3: Forgetting 'new' Keyword
function Person(name) {
this.name = name;
}
// β BAD: Forgot 'new'
const person = Person("Alice");
console.log(person); // undefined
console.log(window.name); // "Alice" (attached to global object!)
// β
GOOD: Use 'new'
const person = new Person("Alice");
The Truth: Classes ARE Prototypes
Let's prove it:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
// Classes are just functions
console.log(typeof Person); // "function"
// Methods go on prototype
console.log(Person.prototype.greet); // [Function: greet]
// Instances have Person.prototype as prototype
const alice = new Person("Alice");
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
// You can even add methods like constructor functions!
Person.prototype.sayGoodbye = function() {
console.log(`Goodbye, ${this.name}`);
};
alice.sayGoodbye(); // "Goodbye, Alice"
Classes are just prettier syntax for the prototype system!
Modern Best Practice: Use Classes (But Understand Prototypes)
Recommendation:
// β
Use ES6 classes for new code
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} (${this.email})`;
}
}
// But know they use prototypes
console.log(User.prototype.getInfo); // [Function: getInfo]
Why?
- β Cleaner, more readable syntax
- β Easier for developers from other languages
- β Better tooling support
- β Standard in modern JavaScript
But remember:
- Classes are syntactic sugar
- Everything still uses prototypes
- Understanding prototypes helps you debug
- Knowing prototypes makes you a better JS developer
Key Differences Summary
| Aspect | Constructor Functions | ES6 Classes |
|---|---|---|
| Syntax | function Person() {} | class Person {} |
| Methods | Person.prototype.method = function() {} | method() {} inside class |
| Inheritance | Child.prototype = Object.create(Parent.prototype) | class Child extends Parent {} |
| Super constructor | Parent.call(this, args) | super(args) |
| Readability | More verbose | Cleaner |
| Under the hood | Pure prototypes | Prototypes + syntax sugar |
Common Mistakes to Avoid
1. Forgetting to Call super()
class Student extends Person {
constructor(name, grade) {
// Forgot super(name)!
this.grade = grade; // ReferenceError: Must call super
}
}
2. Modifying Built-in Prototypes
// Bad - pollutes global namespace
Array.prototype.myMethod = function() {
// This affects ALL arrays!
};
// Good - use utility functions instead
function myArrayMethod(arr) {
// Doesn't modify prototype
}
3. Confusing Instance Properties vs Prototype Methods
// Inefficient - new function for each instance
class User {
constructor(name) {
this.name = name;
this.greet = function() { // Creates new function every time!
return `Hi, ${this.name}`;
};
}
}
// Better - shared on prototype
class User {
constructor(name) {
this.name = name;
}
greet() { // Shared across all instances
return `Hi, ${this.name}`;
}
}
4. Not Understanding this Binding
Remember: this in methods depends on how they're called. Use arrow functions or .bind() when passing methods as callbacks!
Conclusion: Embrace Classes, Understand Prototypes
For day-to-day coding:
- β Use ES6 classes (modern, clean, readable)
- β
Use
extendsfor inheritance - β
Use
super()to call parent methods
For deeper understanding:
- β Learn how prototypes work
- β Understand the prototype chain
- β Know that classes are syntax sugar
Remember:
- JavaScript's OOP is prototypal, not classical
- Classes are just prettier syntax over prototypes
- Every object has a prototype (except
Object.prototype) - The prototype chain enables inheritance
Master both concepts, and you'll be a JavaScript OOP expert!
Building JavaScript applications with classes and prototypes? I'd love to hear about your experience! Connect with me on Twitter or LinkedIn!
Support My Work
If this guide helped you understand JavaScript prototypes vs classes, make the right choice for your projects, or deepen your JavaScript knowledge, I'd really appreciate your support! Creating comprehensive, practical JavaScript tutorials like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for JavaScript developers.
β Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!
Cover image by Tool., Inc on Unsplash