Classes
ES6 syntax for object-oriented programming — constructors, methods, inheritance, and private fields.
1. Introduction
JavaScript has always supported object-oriented patterns via prototypes. ES6 classes provide a cleaner, familiar syntax on top of that prototype system — they are syntactic sugar, not a new mechanism. Classes are used in React (class components), in custom web components, and wherever you need to create many objects sharing the same structure and behaviour.
2. Theory
2.1 Basic class syntax
class User {
// constructor — runs when you do 'new User(...)'
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date().toISOString();
}
// Method — shared by all instances (on the prototype)
greet() {
return `Hello, I'm ${this.name}`;
}
describe() {
return `${this.name} <${this.email}>`;
}
}
// Create instances
const alice = new User('Alice', 'alice@example.com');
const bob = new User('Bob', 'bob@example.com');
console.log(alice.greet()); // "Hello, I'm Alice"
console.log(bob.describe()); // "Bob <bob@example.com>"
console.log(alice instanceof User); // true
2.2 Getters and setters
class Temperature {
constructor(celsius) {
this._celsius = celsius; // _ convention = "intended as private"
}
get celsius() { return this._celsius; }
get fahrenheit() { return this._celsius * 9/5 + 32; }
get kelvin() { return this._celsius + 273.15; }
set celsius(value) {
if (value < -273.15) throw new RangeError('Below absolute zero');
this._celsius = value;
}
}
const temp = new Temperature(100);
console.log(temp.celsius); // 100
console.log(temp.fahrenheit); // 212
console.log(temp.kelvin); // 373.15
temp.celsius = 0;
console.log(temp.fahrenheit); // 32
2.3 Static methods and properties
class MathHelper {
// Static — called on the CLASS, not instances
static add(a, b) { return a + b; }
static subtract(a, b) { return a - b; }
static clamp(n, min, max) {
return Math.min(Math.max(n, min), max);
}
}
console.log(MathHelper.add(2, 3)); // 5
console.log(MathHelper.clamp(15, 0, 10)); // 10
// Static factory method — creates instances with validation
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
static fromObject({ name, email }) {
if (!name || !email) throw new Error('Name and email required');
return new User(name.trim(), email.toLowerCase());
}
}
const user = User.fromObject({ name: ' Alice ', email: 'ALICE@EXAMPLE.COM' });
console.log(user.name); // 'Alice'
console.log(user.email); // 'alice@example.com'
2.4 Inheritance — extends and super
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;
}
fetch(item) {
return `${this.name} fetches the ${item}!`;
}
// Override parent method
speak() {
return `${super.speak()} (${this.breed})`;
}
}
const rex = new Dog('Rex', 'Labrador');
console.log(rex.speak()); // "Rex says Woof! (Labrador)"
console.log(rex.fetch('ball')); // "Rex fetches the ball!"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true — Dog IS-A Animal
2.5 Private fields (modern syntax)
// Private fields — truly inaccessible outside the class
class BankAccount {
#balance = 0; // private — only accessible inside the class
#owner;
constructor(owner, initialBalance = 0) {
this.#owner = owner;
this.#balance = initialBalance;
}
get balance() { return this.#balance; }
get owner() { return this.#owner; }
deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
this.#balance += amount;
return this;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
return this;
}
toString() {
return `${this.#owner}: $${this.#balance.toFixed(2)}`;
}
}
const acct = new BankAccount('Alice', 1000);
acct.deposit(500).withdraw(200);
console.log(acct.balance); // 1300
console.log(acct.toString()); // "Alice: $1300.00"
// acct.#balance; // SyntaxError — truly private
2.6 Static private fields and methods
class IdGenerator {
static #nextId = 1;
static generate() {
return IdGenerator.#nextId++;
}
}
console.log(IdGenerator.generate()); // 1
console.log(IdGenerator.generate()); // 2
console.log(IdGenerator.generate()); // 3
2.7 Class fields (instance properties without constructor)
class Todo {
// Class fields — shorthand for this.xxx in constructor
id = IdGenerator.generate();
done = false;
createdAt = Date.now();
constructor(text, priority = 'medium') {
this.text = text;
this.priority = priority;
// id, done, createdAt already set by class fields above
}
toggle() {
this.done = !this.done;
return this;
}
}
const t = new Todo('Learn ES6 Classes');
console.log(t.id); // auto-generated
console.log(t.done); // false
t.toggle();
console.log(t.done); // true
2.8 When to use classes vs factory functions
// Use CLASSES when:
// - You need inheritance (extends)
// - You work with frameworks that expect classes (React class components, Angular)
// - You need instanceof checks
// - You want private fields with # syntax
// Use FACTORY FUNCTIONS when:
// - Simple object creation with no inheritance
// - You want true privacy via closures
// - Functional programming style is preferred
// - Simpler testing and composition
// Both are valid — modern JS leans toward classes for OOP patterns,
// factory functions for simpler use cases
3. Real World Example
// EventEmitter class — pub/sub pattern
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
return this; // chainable
}
off(event, callback) {
if (!this.#listeners.has(event)) return this;
const filtered = this.#listeners.get(event).filter(cb => cb !== callback);
this.#listeners.set(event, filtered);
return this;
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(cb => cb(...args));
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
// Usage
class Store extends EventEmitter {
#state;
constructor(initialState) {
super(); // must call super() when extending
this.#state = initialState;
}
get state() { return { ...this.#state }; }
setState(changes) {
this.#state = { ...this.#state, ...changes };
this.emit('change', this.state);
}
}
const store = new Store({ count: 0, user: null });
store.on('change', state => {
console.log('State changed:', state);
});
store.setState({ count: 1 }); // "State changed: { count: 1, user: null }"
store.setState({ user: 'Alice' }); // "State changed: { count: 1, user: 'Alice' }"
4. Code Example
<div id="output"></div>
<script>
class Shape {
constructor(color = 'black') {
this.color = color;
}
area() { return 0; }
perimeter() { return 0; }
describe() {
return `${this.constructor.name} — color: ${this.color}, ` +
`area: ${this.area().toFixed(2)}, perimeter: ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
#radius;
constructor(radius, color) {
super(color);
this.#radius = radius;
}
get radius() { return this.#radius; }
area() { return Math.PI * this.#radius ** 2; }
perimeter() { return 2 * Math.PI * this.#radius; }
}
class Rectangle extends Shape {
constructor(width, height, color) {
super(color);
this.width = width;
this.height = height;
}
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
}
class Square extends Rectangle {
constructor(side, color) {
super(side, side, color); // square is a rectangle with equal sides
}
}
const shapes = [
new Circle(5, 'red'),
new Rectangle(4, 6, 'blue'),
new Square(3, 'green')
];
const output = document.querySelector('#output');
shapes.forEach(shape => {
const p = document.createElement('p');
p.textContent = shape.describe();
output.appendChild(p);
});
// Polymorphism — same method call, different results
const totalArea = shapes.reduce((sum, s) => sum + s.area(), 0);
const p = document.createElement('p');
p.textContent = `Total area: ${totalArea.toFixed(2)}`;
output.appendChild(p);
</script>
5. Code Breakdown
Inheritance chain
Square extends Rectangle which extends Shape. super(side, side, color) calls Rectangle's constructor with equal width and height. describe() is defined only in Shape but works for all three shapes through prototype inheritance.
this.constructor.name
Every class instance has constructor pointing back to its class. constructor.name is the class name as a string — useful for logging and debugging without hardcoding strings.
Polymorphism
The shapes.reduce loop calls s.area() on every shape. Each shape's class provides its own area() implementation. The same code works for circles, rectangles, and squares — this is polymorphism.
Private field with getter
Circle's #radius is private (can't be accessed outside). The public get radius() provides read-only access — callers can read the value but not change it directly.
6. Common Mistakes
Mistake 1 — Forgetting super() in a subclass constructor
class Animal { constructor(name) { this.name = name; } }
class Dog extends Animal {
constructor(name, breed) {
this.breed = breed; // ReferenceError! Must call super() first
super(name);
}
}
// Fix: call super() before any this.xxx
Mistake 2 — Using arrow function as a class method
class Widget {
// Arrow as class field — creates a new function per instance (ok for event binding)
handleClick = () => { console.log(this); }; // 'this' = instance — useful for handlers
// But for regular methods — use method syntax (shared on prototype)
render() { } // one function shared by all instances — correct
}
// Arrow fields are sometimes intentional for event handlers (preserves this when detached)
// Regular methods are better for general-purpose methods
Mistake 3 — Classes are NOT hoisted like function declarations
const u = new User(); // ReferenceError! Class not yet defined
class User { constructor() {} }
// Fix: define the class before using it
Mistake 4 — Deep inheritance hierarchies
// Avoid chaining many extends (A extends B extends C extends D...)
// It creates tight coupling and makes code hard to refactor
// Prefer composition — give objects capabilities instead of inheriting them
// "Prefer composition over inheritance" is a core OOP principle
7. Best Practices
- Use private fields (#) for internal state — prevents accidental external mutation.
- Always call super() first in subclass constructors before accessing this.
- Keep inheritance shallow — one or two levels is usually enough; prefer composition over deep hierarchies.
- Use static factory methods for complex construction and validation logic.
- Use getters for computed properties rather than methods — they read like properties but compute on demand.
- Classes are not required — factory functions are perfectly valid; choose based on context and team convention.
8. Practice Exercise
- Create a
Stackclass with private#itemsarray. Implement:push(item),pop(),peek(),isEmpty(), and asizegetter. Test thoroughly. - Create a
Vehiclebase class andCar,Trucksubclasses. Each has different fuel efficiency calculations. Implement afuelCost(distance, pricePerLitre)method using polymorphism. - Add a static
Vehicle.fromJSON(jsonString)factory method that parses a JSON string and returns the correct subclass instance based on atypefield.
9. Assignment
Build an "Animal Kingdom" simulation using classes.
- Create:
Animal(base — name, age, sound, energy),Mammal extends Animal(adds warmBlooded, furColor),Bird extends Animal(adds wingspan, canFly),Dog extends Mammal(adds breed, fetch method),Eagle extends Bird(adds altitude, dive method). - Each class has: a constructor, a
speak()method, aeat(amount)method (increases energy), atoString()method. - Use private fields for
#energy. Publicget energy()returns it.eatand actions decrease it. - Render all animals in the DOM — clicking one makes it speak and shows its stats.
Deliverable: One HTML file.
10. Interview Questions
- What is a JavaScript class?
Syntactic sugar over the prototype system. A class defines a constructor function and methods shared by all instances via the prototype. It provides a cleaner OOP syntax but doesn't add new runtime behaviour. - What does the extends keyword do?
Sets up prototype chain inheritance — the subclass inherits all methods from the parent class. The subclass constructor must call super() before accessing this. - What is the difference between instance methods and static methods?
Instance methods are called on instances (obj.method()) and have access to this. Static methods are called on the class itself (ClassName.method()) and cannot access instance properties via this — they are utility or factory functions. - What are private class fields?
Properties declared with a # prefix that are truly inaccessible outside the class body — unlike the _ convention which is just a naming hint. Private fields prevent accidental external access and are enforced by the JavaScript engine. - What does super() do?
In a constructor, super() calls the parent class's constructor — required in any class that extends another before this can be used. In a method, super.method() calls the overridden version of the method from the parent class.
11. Additional Resources
- MDN — Classes
- javascript.info — Classes — excellent multi-part series
- MDN — Private class features
- MDN — extends and super