Classes and Object-Oriented Programming
Overview
Classes in JavaScript provide a syntactic sugar over JavaScript's prototype-based inheritance. Introduced in ES6 (2015), classes make it easier to create objects with shared behavior and state, enabling object-oriented programming patterns.
What is a Class?
A class is a blueprint for creating objects. It defines properties (data) and methods (behavior) that instances of the class will have.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
const dog = new Animal('Dog');
dog.speak(); // Output: Dog makes a sound
Class Components
- Constructor: Special method called when creating a new instance. Used to initialize properties
- Properties: Data stored in the instance
- Methods: Functions that belong to the class
- this: Refers to the current instance
Constructors
The constructor() method is called automatically when you create a new instance using the new keyword.
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
isAdult() {
return this.age >= 18;
}
}
const person = new Person('John', 'Doe', 25);
console.log(person.getFullName()); // John Doe
console.log(person.isAdult()); // true
Instance Properties and Methods
Instance Properties
Properties are stored on each individual instance:
class BankAccount {
constructor(owner, balance = 0) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
return this.balance;
}
withdraw(amount) {
if (amount > this.balance) {
console.log('Insufficient funds');
return null;
}
this.balance -= amount;
return this.balance;
}
}
const account = new BankAccount('Alice', 1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
Instance Methods
Methods define behavior. They have access to this:
class Car {
constructor(brand, model, speed = 0) {
this.brand = brand;
this.model = model;
this.speed = speed;
}
accelerate() {
this.speed = Math.min(this.speed + 10, 200);
return `${this.brand} ${this.model} is now going ${this.speed} km/h`;
}
brake() {
this.speed = Math.max(this.speed - 10, 0);
return `${this.brand} ${this.model} slowed down to ${this.speed} km/h`;
}
}
const car = new Car('Tesla', 'Model 3');
console.log(car.accelerate()); // Tesla Model 3 is now going 10 km/h
console.log(car.accelerate()); // Tesla Model 3 is now going 20 km/h
Static Methods and Properties
Static members belong to the class itself, not to instances:
class MathUtils {
static PI = 3.14159;
static E = 2.71828;
static circleArea(radius) {
return this.PI * radius * radius;
}
static cylinderVolume(radius, height) {
return this.circleArea(radius) * height;
}
}
// Access static members via class name, not instance
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.circleArea(5)); // 78.53975
console.log(MathUtils.cylinderVolume(5, 10)); // 785.3975
// Instances don't have access to static members
const utils = new MathUtils();
console.log(utils.PI); // undefined
Inheritance
Inheritance allows a class to extend another class, inheriting its properties and methods:
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
}
describe() {
return `${this.name} is a ${this.species}`;
}
makeSound() {
return 'Some generic sound';
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, 'Dog'); // Call parent constructor
this.breed = breed;
}
makeSound() {
return 'Woof! Woof!';
}
getBreed() {
return `${this.name} is a ${this.breed} dog`;
}
}
const dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.describe()); // Buddy is a Dog
console.log(dog.makeSound()); // Woof! Woof!
console.log(dog.getBreed()); // Buddy is a Golden Retriever dog
Using super
The super keyword calls the parent class's method:
class Vehicle {
constructor(brand) {
this.brand = brand;
}
start() {
return `${this.brand} is starting...`;
}
}
class ElectricVehicle extends Vehicle {
constructor(brand, batteryCapacity) {
super(brand);
this.batteryCapacity = batteryCapacity;
}
start() {
// Call parent method and extend it
return super.start() + ' (Battery charging)';
}
chargeBattery(hours) {
return `${this.brand} battery charged for ${hours} hours`;
}
}
const tesla = new ElectricVehicle('Tesla', 100);
console.log(tesla.start()); // Tesla is starting... (Battery charging)
console.log(tesla.chargeBattery(2)); // Tesla battery charged for 2 hours
Getters and Setters
Getters and setters provide controlled access to object properties:
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
get perimeter() {
return 2 * (this._width + this._height);
}
set width(value) {
if (value <= 0) {
console.log('Width must be positive');
return;
}
this._width = value;
}
set height(value) {
if (value <= 0) {
console.log('Height must be positive');
return;
}
this._height = value;
}
}
const rect = new Rectangle(10, 20);
console.log(rect.area); // 200 (getter - no parentheses)
console.log(rect.perimeter); // 60
rect.width = 15; // setter
console.log(rect.area); // 300
Private Fields
Private fields (ES2022) are only accessible within the class:
class BankAccount {
#balance = 0; // Private field
#pinCode;
constructor(owner, pinCode) {
this.owner = owner;
this.#pinCode = pinCode;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return `Deposited $${amount}`;
}
}
withdraw(amount, pin) {
if (pin === this.#pinCode && amount <= this.#balance) {
this.#balance -= amount;
return `Withdrew $${amount}`;
}
return 'Transaction failed';
}
getBalance(pin) {
if (pin === this.#pinCode) {
return this.#balance;
}
return 'Invalid PIN';
}
}
const account = new BankAccount('Alice', 1234);
account.deposit(1000);
console.log(account.getBalance(1234)); // 1000
console.log(account.#balance); // SyntaxError: Private field
Encapsulation with Naming Conventions
Before private fields, developers used naming conventions:
class User {
constructor(username, password) {
this.username = username;
this._password = password; // Convention: "private" by naming
}
setPassword(newPassword) {
if (newPassword.length < 8) {
console.log('Password must be at least 8 characters');
return false;
}
this._password = newPassword;
return true;
}
verifyPassword(inputPassword) {
return inputPassword === this._password;
}
}
const user = new User('john', 'secretpass');
console.log(user.setPassword('newpass123')); // true
Polymorphism
Polymorphism allows different classes to implement the same method differently:
class Shape {
getArea() {
throw new Error('Method must be implemented');
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
// Polymorphism: same method, different behavior
const shapes = [
new Circle(5),
new Square(4),
new Circle(3)
];
shapes.forEach(shape => {
console.log(shape.getArea().toFixed(2));
});
// Output: 78.54, 16.00, 28.27
Composition vs Inheritance
Composition is often preferable to inheritance:
class Engine {
start() {
return 'Engine started';
}
stop() {
return 'Engine stopped';
}
}
class GPS {
navigate(destination) {
return `Navigating to ${destination}`;
}
}
class Car {
constructor(brand) {
this.brand = brand;
this.engine = new Engine();
this.gps = new GPS();
}
start() {
return this.engine.start();
}
stop() {
return this.engine.stop();
}
goTo(destination) {
return this.gps.navigate(destination);
}
}
const car = new Car('Toyota');
console.log(car.start()); // Engine started
console.log(car.goTo('Downtown')); // Navigating to Downtown
Abstract Classes Pattern
JavaScript doesn't have native abstract classes, but you can simulate them:
class Animal {
constructor(name) {
if (new.target === Animal) {
throw new Error('Cannot instantiate abstract class');
}
this.name = name;
}
speak() {
throw new Error('Method speak() must be implemented');
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}
// const animal = new Animal('Generic'); // Error
const dog = new Dog('Buddy');
console.log(dog.speak()); // Buddy barks
Common Patterns
Singleton Pattern
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
this.connection = 'Connected to database';
return this.connection;
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
Factory Pattern
class Animal {
speak() {
throw new Error('Must implement speak()');
}
}
class Dog extends Animal {
speak() { return 'Woof'; }
}
class Cat extends Animal {
speak() { return 'Meow'; }
}
class AnimalFactory {
static createAnimal(type) {
if (type === 'dog') return new Dog();
if (type === 'cat') return new Cat();
throw new Error('Unknown animal type');
}
}
const dog = AnimalFactory.createAnimal('dog');
const cat = AnimalFactory.createAnimal('cat');
Prototypes (Behind the Scenes)
Classes are syntactic sugar over JavaScript's prototype-based model:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
// Under the hood, methods are added to the prototype
console.log(Animal.prototype.speak); // [Function: speak]
console.log(Animal.prototype.constructor === Animal); // true
const dog = new Animal('Dog');
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
Best Practices
- Use meaningful class names: Classes should represent real-world entities
- Keep classes focused: A class should have a single responsibility
- Use private fields for sensitive data: Prevent accidental access
- Prefer composition to inheritance: More flexible and maintainable
- Validate input in constructors: Ensure valid initial state
- Use getters/setters for computed properties: Clean, readable code
- Document your classes: Explain purpose and usage
class User {
#passwordHash;
constructor(email, passwordHash, role = 'user') {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
this.email = email;
this.#passwordHash = passwordHash;
this.role = role;
this.createdAt = new Date();
}
verifyPassword(password) {
// In real code, use bcrypt or similar
return this.#passwordHash === password;
}
isAdmin() {
return this.role === 'admin';
}
get accountAge() {
return new Date() - this.createdAt;
}
}
const user = new User('john@example.com', 'hashed', 'admin');
console.log(user.isAdmin()); // true
console.log(user.email); // john@example.com
Key Takeaways
- Classes provide a cleaner syntax for object creation and inheritance
- Use
constructorto initialize instances - Static members belong to the class, not instances
extendsenables inheritance;supercalls parent methods- Private fields protect internal state
- Composition is often better than deep inheritance hierarchies
- Classes are syntactic sugar over prototypes