Classes & Inheritance

Difficulty: Intermediate

Question

Explain ES6 classes in JavaScript. How do inheritance, static methods, and private fields work?

Answer

ES6 classes are syntactic sugar over JavaScript's prototype-based inheritance. They provide a clearer, more familiar syntax for creating constructor functions and setting up prototype chains.

A class body can contain a constructor, instance methods, static methods, getters/setters, and private fields (prefixed with #). The 'extends' keyword creates a subclass, and 'super' calls the parent class constructor or methods.

Static methods belong to the class itself, not instances. Private fields (ES2022) with the # prefix are truly private - they cannot be accessed outside the class, unlike the older _convention which was just a naming hint.

Despite the class syntax, understanding that classes are functions and inheritance is prototypal remains important for interviews.

Code examples

Class Basics with Inheritance

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name} makes a sound`;
  }

  static create(name) {
    return new this(name);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // must call super before using 'this'
    this.breed = breed;
  }

  speak() {
    return `${this.name} barks`;
  }

  info() {
    return `${super.speak()} (actually: ${this.speak()})`;
  }
}

const dog = Dog.create('Rex');
console.log(dog.speak());

const buddy = new Dog('Buddy', 'Labrador');
console.log(buddy.info());
console.log(buddy instanceof Dog);    // true
console.log(buddy instanceof Animal); // true

extends sets up the prototype chain. super() in the constructor calls the parent constructor. super.method() calls the parent's version of an overridden method.

Private Fields and Getters/Setters

class BankAccount {
  #balance;  // private field
  #owner;

  constructor(owner, initialBalance) {
    this.#owner = owner;
    this.#balance = initialBalance;
  }

  get balance() {
    return `${this.#balance.toFixed(2)}`;
  }

  set balance(value) {
    throw new Error('Use deposit() or withdraw()');
  }

  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}'s account: ${this.balance}`;
  }
}

const acct = new BankAccount('Alice', 1000);
acct.deposit(500).withdraw(200); // chaining
console.log(acct.toString());
console.log(acct.balance);
// console.log(acct.#balance); // SyntaxError: private field

Private fields with # are truly private - accessing them outside the class is a SyntaxError. Getters/setters let you control property access with method logic.

Static Methods and Properties

class MathUtils {
  static PI = 3.14159;

  static add(a, b) { return a + b; }
  static multiply(a, b) { return a * b; }

  static #formatResult(result) {
    return `Result: ${result}`;
  }

  static calculate(operation, a, b) {
    const ops = { add: this.add, multiply: this.multiply };
    const result = ops[operation]?.(a, b);
    return result !== undefined
      ? this.#formatResult(result)
      : 'Unknown operation';
  }
}

console.log(MathUtils.PI);
console.log(MathUtils.add(2, 3));
console.log(MathUtils.calculate('multiply', 4, 5));

// Static methods are NOT on instances
const m = new MathUtils();
console.log(typeof m.add); // undefined

Static methods and properties belong to the class, not instances. They are useful for utility functions and factory methods. Private static methods use #.

Key points

Concepts covered

class, extends, super, static, Private Fields, Getters/Setters