Classes

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

JavaScript classes are, under the hood, prototype-based: two objects are members of the same class if they inherit from the same prototype object. The class keyword introduced in ES6 does not change this mechanism — it is syntactic sugar over the constructor-function-plus-prototype pattern described in Legacy Features to Avoid — but it packages that mechanism into a much cleaner, more familiar syntax. This page covers the modern class syntax exclusively; you only need the legacy pattern to read older code.

Class Declarations and Expressions

A class is declared with the class keyword, a name, and a body in curly braces:

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  includes(x) {
    return this.from <= x && x <= this.to;
  }

  toString() {
    return `(${this.from}...${this.to})`;
  }
}

const r = new Range(1, 3);
r.includes(2);   // => true
r.toString();    // => "(1...3)"

A few things are worth noting:

  • Method bodies use the object-literal shorthand syntax (no function keyword), and are not separated by commas.

  • constructor defines the function that runs when the class is instantiated with new. If you omit it, an empty constructor is created for you implicitly.

  • Class bodies are always evaluated in strict mode, even without a "use strict" directive.

  • Unlike function declarations, class declarations are not hoisted — you cannot instantiate a class before its declaration is evaluated.

Classes also have an expression form, useful when a class needs to be produced dynamically (e.g. a factory function that returns a subclass):

const Square = class {
  constructor(x) {
    this.area = x * x;
  }
};

new Square(3).area;   // => 9

Instance Fields and Methods

Instance methods, defined in the class body, become properties of the class’s prototype and are shared by every instance. Instance fields (data properties unique to each instance) can be declared directly in the class body, with or without an initializer — the declaration runs as part of construction, before the constructor body:

class Buffer {
  size = 0;
  capacity = 4096;
  data = new Uint8Array(this.capacity);   // `this` is available in field initializers
}

This is equivalent to assigning this.size = 0, etc., at the top of the constructor, but keeps every field declaration visible at a glance instead of buried in constructor logic.

Private Fields and Methods

Prefixing a field or method name with # makes it private: usable from within the class body, but invisible and inaccessible (a SyntaxError, not just undefined) from any code outside it. Private fields must be declared in the class body before use — you cannot add one dynamically from inside the constructor.

class Buffer {
  #size = 0;

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

  #grow(by) {
    this.#size += by;   // private method, only callable from inside this class
  }
}

const b = new Buffer();
b.size;      // => 0 (via the public getter)
b.#size;     // SyntaxError: private field '#size' must be declared in an enclosing class

Private fields are a strong tool for enforcing encapsulation: nothing outside the class — not even a subclass — can read or mutate them directly, which is exactly why classes that expose mutable internal state (a type-checked map, a buffer’s length) are good candidates for turning that state private.

Static Members

Prefixing a field or method with static attaches it to the constructor function itself, rather than to the prototype (and therefore to instances). Static methods are invoked on the class, never on an instance:

class Range {
  // ...instance members omitted...

  static integerPattern = /^\((\d+)\.\.\.(\d+)\)$/;

  static parse(s) {
    const matches = s.match(Range.integerPattern);
    if (!matches) {
      throw new TypeError(`Cannot parse Range from "${s}"`);
    }
    return new Range(Number(matches[1]), Number(matches[2]));
  }
}

Range.parse('(1...10)');   // => Range instance

Because static methods are called on the constructor, this inside one almost never refers to an instance — it refers to the constructor itself, which is occasionally useful (e.g. new this(…​) in a static factory that should also work correctly on subclasses).

Static members can also be private (static #cache = new Map()), combining constructor-level storage with the same external inaccessibility private instance members get.

Getters and Setters

Getter and setter methods work exactly as they do in object literals (see Objects, Properties & Destructuring), letting a method be accessed with plain property syntax instead of a function call — the only syntactic difference inside a class body is that no comma follows a getter/setter definition:

class Complex {
  constructor(real, imaginary) {
    this.r = real;
    this.i = imaginary;
  }

  get magnitude() {
    return Math.hypot(this.r, this.i);
  }

  set magnitude(value) {
    const scale = value / this.magnitude;
    this.r *= scale;
    this.i *= scale;
  }
}

Getters paired with a private backing field are the idiomatic way to expose read-only (or validated-write) access to otherwise-private state.

Abstract-Class Patterns

JavaScript has no native abstract keyword or formal notion of an abstract class or method. The idiomatic way to express "subclasses must implement this" is a base-class method whose body simply throws:

class AbstractSet {
  has(x) {
    throw new Error('Abstract method: subclasses must implement has()');
  }
}

class RangeSet extends AbstractSet {
  constructor(from, to) {
    super();
    this.from = from;
    this.to = to;
  }

  // Concrete implementation of the "abstract" method inherited above
  has(x) {
    return x >= this.from && x <= this.to;
  }
}

An abstract base class can also implement concrete methods built entirely on top of the abstract ones it declares (isEmpty() built on an abstract size getter, for example), so subclasses only need to fill in a small number of primitive operations and get the rest of the behavior for free. This is a common pattern for building small class hierarchies without a language-level abstract construct.

Subclassing

extends creates a subclass whose instances inherit from the superclass’s prototype, and whose constructor chain runs through super():

class TypedMap extends Map {
  constructor(keyType, valueType, entries) {
    super();                    // must run before `this` can be used below
    this.keyType = keyType;
    this.valueType = valueType;
    if (entries) {
      for (const [key, value] of entries) this.set(key, value);
    }
  }

  // Overrides Map.prototype.set()
  set(key, value) {
    if (typeof key !== this.keyType) {
      throw new TypeError(`${key} is not of type ${this.keyType}`);
    }
    if (typeof value !== this.valueType) {
      throw new TypeError(`${value} is not of type ${this.valueType}`);
    }
    return super.set(key, value);   // delegate to the superclass implementation
  }
}

Rules to know when working with super():

  • A subclass constructor must call super() before it can use this — superclasses always get to initialize themselves first.

  • If a subclass defines no constructor at all, one is created implicitly that just forwards its arguments to super().

  • Inside an overriding method (as opposed to the constructor), calling super.method() is optional and can happen at the start, middle, or end of the override — there’s no ordering requirement the way there is for super() in a constructor.

  • new.target inside a superclass constructor refers to whichever constructor was actually invoked with new — the subclass’s, if the call originated from a subclass — which is occasionally useful for logging or validation, though a well-designed superclass usually shouldn’t need to know it has been subclassed.

extends is convenient, but it is not the only way to reuse another class’s behavior. Composing a class out of an internal instance of another class (delegation, sometimes called "favor composition over inheritance") is often more flexible than a formal extends relationship — reach for it when you find yourself extending a built-in like Set or Map just to reuse a handful of its methods rather than because your class truly is a specialization of it.

Class Hierarchy Diagram

The following diagram shows a minimal two-level hierarchy: an abstract base class declaring has(), and a concrete subclass that overrides it.

classDiagram class AbstractSet { +has(x) Error } class RangeSet { -from -to +has(x) boolean } AbstractSet <|-- RangeSet : extends note for AbstractSet "has() throws — subclasses\nmust override it" note for RangeSet "has() overrides the abstract\nmethod with a real check"

See Also