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
functionkeyword), and are not separated by commas. -
constructordefines the function that runs when the class is instantiated withnew. 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 usethis— 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 forsuper()in a constructor. -
new.targetinside a superclass constructor refers to whichever constructor was actually invoked withnew— 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.
See Also
-
Legacy Features to Avoid — the pre-ES6 constructor-function pattern that
classcompiles down to conceptually. -
Objects, Properties & Destructuring — getter/setter syntax in plain object literals.
-
Modules — organizing one class (or a small family of related classes) per module file.
-
Standard Library: Dates, Errors & JSON — subclassing the built-in
Errorclass to define custom error types.