Classes

Course: JavaScript Advanced

Lesson 6: Classes

Level

Advanced

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Define classes with constructors, instance methods, and static methods
  • Use extends and super to implement inheritance between classes
  • Explain what ES6 class syntax compiles to under the hood and when to prefer composition

Frontend Development Context

ES6 classes are standard in modern JavaScript frameworks. React class components, custom DOM element classes, and many UI libraries use the class syntax. Even if you primarily write functional React components today, you will read and maintain class-based code, extend built-in browser classes like Error and HTMLElement, and encounter class-based APIs in libraries and SDKs.

Understanding classes at an advanced level means knowing not just the syntax but also what the engine is actually doing — placing methods on the prototype, using super to call the parent constructor, and recognizing when a class hierarchy is becoming too deep to be maintainable. It also means knowing when composition (wrapping objects rather than extending them) is a better design choice than inheritance.

Explanation

A class in JavaScript is syntactic sugar over constructor functions and prototype assignment. When you define a class with methods in its body, those methods are placed on the class's prototype object. When you call new MyClass(), the engine creates a new object, sets its prototype to MyClass.prototype, runs the constructor, and returns the new object. This is identical to what prototype-based code does manually.

The constructor is a special method that runs once when the instance is created. It receives the arguments passed to new and is the right place to initialize instance properties. Any property set with this.prop = value inside the constructor becomes an own property of the instance.

Inheritance is achieved with extends. A subclass's constructor must call super(...) before accessing this — this runs the parent constructor and sets up the prototype chain correctly. Omitting super in a subclass constructor throws a ReferenceError. Once super is called, this is available and the subclass can add its own properties.

Static methods are defined with the static keyword and belong to the class itself, not to instances. They are useful for factory methods, utility functions related to the class, and operations that don't depend on instance state. You call them on the class: MyClass.create(), not on an instance.

Private fields, introduced in modern JavaScript with the # prefix, are true instance-level private values that cannot be accessed outside the class body — unlike the closure-based privacy used before this feature existed. Private fields are a valuable tool for enforcing encapsulation in class-based code.

Code Example

This example defines a base UIComponent class with private state, extends it with a Modal subclass, and demonstrates static factory methods and super.

class UIComponent {
  #mounted = false; // private field

  constructor(id, label) {
    this.id = id;
    this.label = label;
  }

  mount() {
    this.#mounted = true;
    console.log(`[${this.id}] "${this.label}" mounted`);
  }

  unmount() {
    this.#mounted = false;
    console.log(`[${this.id}] "${this.label}" unmounted`);
  }

  get isMounted() {
    return this.#mounted;
  }

  toString() {
    return `UIComponent(${this.id}: ${this.label})`;
  }

  static create(id, label) {
    return new UIComponent(id, label);
  }
}

class Modal extends UIComponent {
  #isOpen = false;

  constructor(id, label, { dismissable = true } = {}) {
    super(id, label); // must come before any this access
    this.dismissable = dismissable;
  }

  open() {
    if (!this.isMounted) {
      throw new Error(`Modal "${this.label}" must be mounted before opening`);
    }
    this.#isOpen = true;
    console.log(`Modal "${this.label}" opened`);
  }

  close() {
    if (this.dismissable) {
      this.#isOpen = false;
      console.log(`Modal "${this.label}" closed`);
    }
  }

  get isOpen() { return this.#isOpen; }

  toString() {
    return `Modal(${this.id}: ${this.label}, open=${this.#isOpen})`;
  }

  static createConfirm(id) {
    return new Modal(id, "Confirm Action", { dismissable: false });
  }
}

const banner = UIComponent.create("banner-1", "Welcome Banner");
banner.mount();                  // "[banner-1] "Welcome Banner" mounted"
console.log(banner.isMounted);   // true

const modal = new Modal("modal-1", "Settings", { dismissable: true });
modal.mount();
modal.open();                    // "Modal "Settings" opened"
console.log(modal.isOpen);       // true
modal.close();                   // "Modal "Settings" closed"

const confirm = Modal.createConfirm("confirm-1");
confirm.mount();
confirm.open();
confirm.close(); // no-op — dismissable is false
console.log(confirm.isOpen); // true — still open

Code Explanation

UIComponent defines a private field #mounted that cannot be read or set from outside the class. The isMounted getter provides controlled read access. The static create method is a factory that produces instances without callers needing to use new directly — a common pattern in library APIs.

Modal extends UIComponent. The constructor calls super(id, label) first, which runs UIComponent's constructor and initializes the prototype chain. Only after super completes can this be safely used — the this.dismissable = dismissable line comes after super. Modal adds its own private field #isOpen and new methods open and close.

modal.isMounted is inherited from UIComponent through the prototype chain. When modal.open() is called, it uses this.isMounted to check the parent class's private state through the public getter — private fields are not directly accessible by subclasses, which is the correct encapsulation behavior.

Modal.createConfirm is a static factory on the subclass. It creates a Modal with dismissable: false, so calling close() on it has no effect. This demonstrates static methods on subclasses and how they can encode domain-specific construction logic without cluttering the constructor.

Pattern Highlights

Positive Pattern: Use private fields (#field) to enforce genuine encapsulation — they cannot be accessed or patched from outside the class, unlike the convention of prefixing with _ which is merely a suggestion.

⚠️ Neutral Note: Deep inheritance chains (more than 2–3 levels) become difficult to reason about and change. When you find yourself calling super.super logic mentally through several layers, favor composition: give the class an instance of the other class rather than extending it.

Negative Pattern: Do not access this before calling super() in a derived class constructor — it throws a ReferenceError because the instance is not fully initialized until the parent constructor runs.

Common Mistakes

Mistake 1: Forgetting super() in a subclass constructor

Accessing this before super() in a subclass constructor throws at runtime.

class Tooltip extends UIComponent {
  constructor(id, label, content) {
    this.content = content; // ReferenceError: Must call super constructor first
    super(id, label);
  }
}

Always call super(...) as the very first statement in a subclass constructor before touching this.

Mistake 2: Defining methods inside the constructor

Methods defined inside the constructor are created fresh on every instance, wasting memory.

class Card {
  constructor(title) {
    this.title = title;
    this.render = function () { // new function per instance — avoid this
      return `<div>${this.title}</div>`;
    };
  }
}

Define render in the class body instead. Class body methods are placed on the prototype and shared across all instances.

Mistake 3: Using class inheritance to share utilities

Extending a class just to get access to some utility methods creates an incorrect semantic relationship.

class DateUtils {
  formatDate(date) { return date.toISOString().split("T")[0]; }
}

class UserForm extends DateUtils { // UserForm is NOT a DateUtils
  constructor() {
    super();
    this.joinDate = new Date();
  }
}

If UserForm is not truly a specialized DateUtils, use composition: give UserForm an instance of DateUtils, or import the utility as a plain function. Inheritance should express "is-a" relationships, not "uses-a" relationships.

Quick Recap

  • ES6 class syntax is syntax sugar over constructor functions and prototype assignment — methods in a class body are placed on the prototype, not copied onto each instance.
  • Subclasses use extends and must call super() before accessing this in their constructor; super invokes the parent class's constructor.
  • Static methods belong to the class itself and are called on the class, not instances — useful for factory methods and class-level utilities.
  • Private fields (#field) provide genuine encapsulation that cannot be bypassed from outside the class, unlike convention-based _field prefixes.

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01
Ant Skills - Classes