22 minLesson 26 of 35
OOP & Advanced Concepts
Classes & Prototype Chain
Classes & Prototype Chain
JavaScript is a prototype-based language — objects inherit directly from other objects, not from classes. ES6 class syntax makes this feel familiar to developers from other languages, but it's syntactic sugar over the prototype system. Understanding both is crucial.
ES6 Classes
class User {
// Constructor runs when you use 'new'
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
}
// Method on the prototype (shared by all instances)
greet() {
return `Hello, I'm ${this.name}!`;
}
// Getter
get displayName() {
return this.name.split(" ")[0];
}
// Static method — called on the class, not instances
static create(name, email) {
return new User(name, email);
}
// Static property
static defaultRole = "user";
toString() {
return `User(${this.name})`;
}
}
const alice = new User("Alice Smith", "alice@example.com");
alice.greet(); // "Hello, I'm Alice Smith!"
alice.displayName; // "Alice"
User.create("Bob", "bob@example.com");
User.defaultRole; // "user"
Inheritance
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}`;
}
toString() {
return `Animal(${this.name})`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, "Woof"); // must call super() before using 'this'
this.breed = breed;
}
// Override parent method
speak() {
return `${super.speak()}! *wags tail*`;
}
fetch(item) {
return `${this.name} fetches the ${item}!`;
}
}
const rex = new Dog("Rex", "Labrador");
rex.speak(); // "Rex says Woof! *wags tail*"
rex.fetch("ball"); // "Rex fetches the ball!"
rex instanceof Dog; // true
rex instanceof Animal; // true
The Prototype Chain
Under the hood, JavaScript uses prototypes. Every object has a hidden [[Prototype]] link:
// What actually happens with class syntax:
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const alice = new User("Alice");
// Property lookup chain:
// alice.greet → not on alice → check User.prototype → found!
// alice.toString → not on alice → not on User.prototype → check Object.prototype → found!
Object.getPrototypeOf(alice) === User.prototype; // true
Object.getPrototypeOf(User.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype); // null (end of chain)
// Class syntax creates the same prototype chain
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
Object.getPrototypeOf(rex) === Dog.prototype; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
Object.getPrototypeOf(Animal.prototype) === Object.prototype; // true
Private Fields (ES2022)
class BankAccount {
#balance = 0; // private — can't be accessed outside the class
#owner;
constructor(owner, initialBalance = 0) {
this.#owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
this.#balance += amount;
return this; // allows chaining
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
return this;
}
get balance() {
return this.#balance;
}
get owner() {
return this.#owner;
}
}
const account = new BankAccount("Alice", 1000);
account.deposit(500).withdraw(200);
account.balance; // 1300
account.#balance; // SyntaxError — private field!
Mixins — Multiple Inheritance Pattern
JavaScript doesn't support multiple inheritance directly, but mixins solve it:
// Mixin functions add methods to a class
const Serializable = (Base) => class extends Base {
serialize() {
return JSON.stringify(this);
}
static deserialize(json) {
return Object.assign(new this(), JSON.parse(json));
}
};
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.createdAt = new Date();
this.updatedAt = new Date();
}
touch() {
this.updatedAt = new Date();
return this;
}
};
class User extends Timestamped(Serializable(Object)) {
constructor(name) {
super();
this.name = name;
}
}
const user = new User("Alice");
user.serialize(); // '{"name":"Alice","createdAt":"...","updatedAt":"..."}'
user.createdAt; // Date object
Object.create
// Create an object with a specific prototype
const animalProto = {
speak() { return `${this.name} speaks`; }
};
const dog = Object.create(animalProto);
dog.name = "Rex";
dog.speak(); // "Rex speaks"
// Object.create(null) — object with no prototype
const pureMap = Object.create(null);
pureMap.key = "value";
// No .toString, .hasOwnProperty, etc. — useful as a safe hash map
When to Use Classes
// Use classes for:
// - Objects with identity and behavior
// - When you need instanceof checks
// - Hierarchies with shared methods
class Logger {
constructor(prefix) { this.prefix = prefix; }
log(msg) { console.log(`[${this.prefix}] ${msg}`); }
error(msg) { console.error(`[${this.prefix}] ERROR: ${msg}`); }
}
// Use plain objects + functions for:
// - Data containers (no methods needed)
// - Functional programming style
const user = { name: "Alice", age: 30 }; // no class needed
// Use factory functions for:
// - Closures over private data
// - When you don't need inheritance or instanceof
function createCounter(initial = 0) {
let count = initial; // genuinely private
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
Next lesson: Closures & Lexical Scope — one of JavaScript's most powerful concepts.
📱
Get Notes Free →Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises