JavaScript Classes: Object-Oriented Programming in ES6+
Master JavaScript classes for object-oriented programming. Learn class syntax, inheritance, static methods, private fields, and best practices.
JavaScript Classes: Object-Oriented Programming in ES6+
Classes in JavaScript provide a cleaner, more intuitive syntax for creating objects and implementing inheritance. While they're syntactic sugar over prototypal inheritance, they make object-oriented programming more accessible.
Introduction to Classes
Classes were introduced in ES6 to provide a more familiar syntax for developers coming from class-based languages.
// Class declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// Creating instances
const john = new Person('John', 30);
console.log(john.greet()); // "Hello, I'm John"
// Class expression
const Animal = class {
constructor(type) {
this.type = type;
}
};
Class Syntax and Structure
Constructor Method
class Rectangle {
constructor(width, height) {
// Initialize instance properties
this.width = width;
this.height = height;
// Can perform validation
if (width <= 0 || height <= 0) {
throw new Error('Width and height must be positive');
}
}
}
// Constructor is called automatically
const rect = new Rectangle(10, 20);
console.log(rect.width); // 10
console.log(rect.height); // 20
// Default parameters in constructor
class Circle {
constructor(radius = 1) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
Instance Methods
class Calculator {
constructor(initialValue = 0) {
this.value = initialValue;
this.history = [];
}
// Instance methods
add(n) {
this.value += n;
this.history.push(`Added ${n}`);
return this;
}
subtract(n) {
this.value -= n;
this.history.push(`Subtracted ${n}`);
return this;
}
multiply(n) {
this.value *= n;
this.history.push(`Multiplied by ${n}`);
return this;
}
divide(n) {
if (n === 0) throw new Error('Cannot divide by zero');
this.value /= n;
this.history.push(`Divided by ${n}`);
return this;
}
getResult() {
return this.value;
}
clearHistory() {
this.history = [];
}
}
// Method chaining
const calc = new Calculator(10);
const result = calc.add(5).multiply(2).subtract(10).getResult();
console.log(result); // 20
Getters and Setters
class Temperature {
constructor(celsius = 0) {
this._celsius = celsius;
}
// Getter
get celsius() {
return this._celsius;
}
// Setter with validation
set celsius(value) {
if (value < -273.15) {
throw new Error('Temperature below absolute zero is not possible');
}
this._celsius = value;
}
// Computed getter
get fahrenheit() {
return (this._celsius * 9) / 5 + 32;
}
set fahrenheit(value) {
this._celsius = ((value - 32) * 5) / 9;
}
get kelvin() {
return this._celsius + 273.15;
}
set kelvin(value) {
this.celsius = value - 273.15; // Uses celsius setter validation
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
Static Methods and Properties
class MathUtils {
// Static property
static PI = 3.14159;
// Static methods
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
static factorial(n) {
if (n <= 1) return 1;
return n * MathUtils.factorial(n - 1);
}
static random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
// Called on class, not instance
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.factorial(5)); // 120
// Practical example
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.id = User.generateId();
}
static idCounter = 0;
static generateId() {
return ++User.idCounter;
}
static validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
static fromJSON(json) {
const data = JSON.parse(json);
return new User(data.name, data.email);
}
}
Class Inheritance
Basic Inheritance
// Parent class
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
eat() {
console.log(`${this.name} is eating`);
}
sleep() {
console.log(`${this.name} is sleeping`);
}
describe() {
return `${this.name} is a ${this.species}`;
}
}
// Child class
class Dog extends Animal {
constructor(name, breed) {
// Must call super() before using 'this'
super(name, 'Canine');
this.breed = breed;
}
bark() {
console.log(`${this.name} says: Woof!`);
}
// Override parent method
describe() {
return `${super.describe()}, specifically a ${this.breed}`;
}
}
const myDog = new Dog('Max', 'Golden Retriever');
myDog.eat(); // "Max is eating"
myDog.bark(); // "Max says: Woof!"
console.log(myDog.describe()); // "Max is a Canine, specifically a Golden Retriever"
Multiple Levels of Inheritance
class Vehicle {
constructor(type, wheels) {
this.type = type;
this.wheels = wheels;
}
start() {
console.log('Vehicle starting...');
}
}
class Car extends Vehicle {
constructor(make, model) {
super('car', 4);
this.make = make;
this.model = model;
}
honk() {
console.log('Beep beep!');
}
}
class ElectricCar extends Car {
constructor(make, model, batteryCapacity) {
super(make, model);
this.batteryCapacity = batteryCapacity;
}
charge() {
console.log(`Charging ${this.batteryCapacity}kWh battery...`);
}
// Override with super call
start() {
super.start();
console.log('Electric motor engaged');
}
}
const tesla = new ElectricCar('Tesla', 'Model 3', 75);
tesla.start();
// "Vehicle starting..."
// "Electric motor engaged"
Private Fields and Methods
class BankAccount {
// Private fields (ES2022)
#balance = 0;
#transactions = [];
constructor(accountNumber, initialDeposit = 0) {
this.accountNumber = accountNumber;
if (initialDeposit > 0) {
this.deposit(initialDeposit);
}
}
// Private method
#logTransaction(type, amount) {
this.#transactions.push({
type,
amount,
timestamp: new Date(),
balance: this.#balance,
});
}
deposit(amount) {
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
this.#balance += amount;
this.#logTransaction('deposit', amount);
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
this.#logTransaction('withdrawal', amount);
return this.#balance;
}
getBalance() {
return this.#balance;
}
getTransactionHistory() {
// Return copy to prevent modification
return [...this.#transactions];
}
}
const account = new BankAccount('123456', 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.#balance); // SyntaxError: Private field '#balance'
Advanced Class Patterns
Mixins
// Mixin functions
const Flyable = {
fly() {
console.log(`${this.name} is flying`);
},
land() {
console.log(`${this.name} has landed`);
},
};
const Swimmable = {
swim() {
console.log(`${this.name} is swimming`);
},
dive() {
console.log(`${this.name} is diving`);
},
};
// Mixin helper
function mixin(target, ...sources) {
sources.forEach((source) => {
Object.getOwnPropertyNames(source).forEach((key) => {
if (key !== 'constructor') {
target[key] = source[key];
}
});
});
}
class Duck extends Animal {
constructor(name) {
super(name, 'Duck');
}
}
// Apply mixins
mixin(Duck.prototype, Flyable, Swimmable);
const donald = new Duck('Donald');
donald.fly(); // "Donald is flying"
donald.swim(); // "Donald is swimming"
Abstract Classes
// JavaScript doesn't have built-in abstract classes
// But we can simulate them
class AbstractShape {
constructor() {
if (new.target === AbstractShape) {
throw new TypeError('Cannot instantiate abstract class');
}
}
// Abstract method (must be overridden)
area() {
throw new Error('Method area() must be implemented');
}
// Concrete method
describe() {
return `This shape has an area of ${this.area()}`;
}
}
class Rectangle extends AbstractShape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle extends AbstractShape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
// const shape = new AbstractShape(); // TypeError
const rect = new Rectangle(5, 10);
console.log(rect.describe()); // "This shape has an area of 50"
Factory Pattern
class Animal {
constructor(name, type) {
this.name = name;
this.type = type;
}
}
class Dog extends Animal {
constructor(name) {
super(name, 'dog');
}
speak() {
return 'Woof!';
}
}
class Cat extends Animal {
constructor(name) {
super(name, 'cat');
}
speak() {
return 'Meow!';
}
}
class AnimalFactory {
static create(type, name) {
switch (type.toLowerCase()) {
case 'dog':
return new Dog(name);
case 'cat':
return new Cat(name);
default:
throw new Error(`Unknown animal type: ${type}`);
}
}
}
const pet1 = AnimalFactory.create('dog', 'Buddy');
const pet2 = AnimalFactory.create('cat', 'Whiskers');
Real-World Examples
Component System
class Component {
constructor(props = {}) {
this.props = props;
this.state = {};
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}
render() {
throw new Error('Component must implement render method');
}
}
class Button extends Component {
constructor(props) {
super(props);
this.state = { clicked: false };
}
handleClick = () => {
this.setState({ clicked: true });
if (this.props.onClick) {
this.props.onClick();
}
};
render() {
const button = document.createElement('button');
button.textContent = this.props.text || 'Click me';
button.className = this.state.clicked ? 'clicked' : '';
button.onclick = this.handleClick;
return button;
}
}
Data Validation
class Validator {
constructor() {
this.rules = [];
}
required(message = 'This field is required') {
this.rules.push((value) => {
if (!value || value.length === 0) {
return message;
}
});
return this;
}
minLength(length, message) {
this.rules.push((value) => {
if (value.length < length) {
return message || `Minimum length is ${length}`;
}
});
return this;
}
email(message = 'Invalid email format') {
this.rules.push((value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(value)) {
return message;
}
});
return this;
}
validate(value) {
for (const rule of this.rules) {
const error = rule(value);
if (error) {
return { valid: false, error };
}
}
return { valid: true };
}
}
// Usage
const emailValidator = new Validator().required().email().minLength(5);
console.log(emailValidator.validate('')); // { valid: false, error: 'This field is required' }
console.log(emailValidator.validate('test')); // { valid: false, error: 'Invalid email format' }
console.log(emailValidator.validate('a@b.c')); // { valid: true }
Event Emitter
class EventEmitter {
constructor() {
this.events = new Map();
}
on(event, listener) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(listener);
return this;
}
off(event, listenerToRemove) {
if (!this.events.has(event)) return this;
const listeners = this.events.get(event);
this.events.set(
event,
listeners.filter((listener) => listener !== listenerToRemove)
);
return this;
}
emit(event, ...args) {
if (!this.events.has(event)) return false;
this.events.get(event).forEach((listener) => {
listener.apply(this, args);
});
return true;
}
once(event, listener) {
const onceWrapper = (...args) => {
listener.apply(this, args);
this.off(event, onceWrapper);
};
return this.on(event, onceWrapper);
}
}
// Usage
class Timer extends EventEmitter {
constructor(duration) {
super();
this.duration = duration;
this.elapsed = 0;
}
start() {
this.emit('start');
this.interval = setInterval(() => {
this.elapsed += 100;
this.emit('tick', this.elapsed);
if (this.elapsed >= this.duration) {
this.stop();
this.emit('complete');
}
}, 100);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.emit('stop');
}
}
}
Best Practices
-
Use classes for complex objects
// Good for complex state and behavior class UserAccount { constructor(data) { this.data = data; } // Many methods... } // Simple objects don't need classes const config = { api: 'https://api.example.com', timeout: 5000, };
-
Keep inheritance shallow
// Avoid deep inheritance chains // Prefer composition over inheritance class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} // Getting deep
-
Use private fields for encapsulation
class Service { #apiKey; constructor(apiKey) { this.#apiKey = apiKey; } async request(endpoint) { // Use private field } }
-
Make methods chainable when appropriate
class QueryBuilder { where(condition) { // ... return this; } orderBy(field) { // ... return this; } }
Conclusion
JavaScript classes provide a clean, familiar syntax for object-oriented programming:
- Constructor for initialization
- Methods for behavior
- Inheritance with extends
- Static methods and properties
- Private fields for encapsulation
- Getters/Setters for computed properties
Key takeaways:
- Classes are syntactic sugar over prototypes
- Use extends for inheritance
- Private fields provide true encapsulation
- Prefer composition over deep inheritance
- Classes work great with modern JavaScript patterns
Master classes to write more structured, maintainable JavaScript applications!