ES6+ FeaturesFeatured

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.

By JavaScriptDoc Team
classesoopes6inheritancejavascript

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

  1. 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,
    };
    
  2. Keep inheritance shallow

    // Avoid deep inheritance chains
    // Prefer composition over inheritance
    class Animal {}
    class Mammal extends Animal {}
    class Dog extends Mammal {} // Getting deep
    
  3. Use private fields for encapsulation

    class Service {
      #apiKey;
    
      constructor(apiKey) {
        this.#apiKey = apiKey;
      }
    
      async request(endpoint) {
        // Use private field
      }
    }
    
  4. 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!