Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 12

Classes

This chapter assumes you already know the prototype chain, how `this` binding works in ordinary functions versus arrow functions, and what property descriptors are. Classes are syntax over those mechanics, and most confusion about classes dissolves once you trace what the syntax compiles down to.
Level: AdvancedLength: In depth4 min read

Read at another depth

Classes

This chapter assumes you already know the prototype chain, how this binding works in ordinary functions versus arrow functions, and what property descriptors are. Classes are syntax over those mechanics, and most confusion about classes dissolves once you trace what the syntax compiles down to.

Classes Are Functions

A class declaration creates a function with unconstructible-call semantics:

Code example in javascript
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  dist() {
    return Math.hypot(this.x, this.y);
  }
}

console.log(typeof Point);                    // "function"
console.log(Object.getPrototypeOf(Point.prototype)
            === Object.prototype);            // true

const p = new Point(3, 4);
console.log(p.dist());                        // 5

The differences from a hand-written constructor function are behavioral, not structural:

  • Class bodies run in strict mode regardless of where the class appears.
  • Calling Point(3, 4) without new throws a TypeError; legacy constructor functions silently operate on the global object.
  • Methods defined in the class body are non-enumerable and installed on Point.prototype as writable, non-enumerable data properties. The prototype object itself is non-writable and non-extensible.
  • Class declarations are hoisted but not initialized: accessing the binding before evaluation throws a ReferenceError (the temporal dead zone, same as let).
  • The class name binding inside the class body is an immutable inner binding, so Point = X outside works but a shadowing reassignment inside fails.

Instance Fields, Static Members, and Private State

Since ES2022, class bodies can declare fields directly, including private fields with #:

Code example in javascript
class Counter {
  static instances = 0;        // static field on the constructor
  #count = 0;                  // private instance field

  static from(n) {             // static method
    const c = new Counter();
    c.#count = n;              // private access is lexical, not structural
    return c;
  }

  get value() { return this.#count; }   // accessor pair
  set value(n) { if (n < 0) throw new RangeError('negative'); this.#count = n; }

  tick() { return ++this.#count; }
}

console.log(Counter.instances);        // 0
console.log(Counter.from(10).tick());  // 11

Key details:

  • Instance fields are installed per-instance, not on the prototype. Each object gets its own property slot, which matters for memory in high-count scenarios.
  • Private # fields are hard-private: no external access, not even via Object.getOwnPropertyNames or Reflect. Subclass code cannot reach a parent's private field unless declared in the same class body.
  • static blocks (static { ... }) let you run setup code with access to private state during class evaluation.
  • Field initializers run in declaration order during construction, after super() returns in subclasses — see the gotcha below.

callout warning

Field initializers shadow inherited accessors silently. If a subclass declares x = 1 while the base class defines a set x() accessor, the subclass's own data property replaces the inherited setter for its instances. Prefer explicit constructor assignment when you intend to route through a base accessor.

Subclassing and super

extends builds a real prototype chain between constructors, not just instances:

Code example in javascript
class Base {
  constructor(x) { this.x = x; }
  describe() { return `x=${this.x}`; }
}

class Derived extends Base {
  constructor(x, y) {
    super(x);          // required before touching `this`
    this.y = y;
  }
  describe() {
    return `${super.describe()}, y=${this.y}`;
  }
}

console.log(Object.getPrototypeOf(Derived) === Base);           // true
console.log(Object.getPrototypeOf(Derived.prototype) === Base.prototype); // true

Note the two-chain structure: Derived's prototype is Base (so static members inherit), and Derived.prototype's prototype is Base.prototype (so instance methods inherit). This differs from older mixin patterns that only linked instance prototypes.

super.describe() performs a lookup on Base.prototype and calls it with the current this; it is a static syntactic form, so it cannot be extracted or rebound.

The Method Inheritance Gotcha

Class methods are stored as properties on the prototype and copied nowhere, so "overwriting" a method later mutates it for every instance — including instances already created:

Code example in javascript
class A { run() { return 'A'; } }
const a = new A();
A.prototype.run = () => 'patched';
console.log(a.run());   // "patched"

This is useful for patching and dangerous for libraries that assign methods onto instances versus the prototype. Contrast: fields, because they are per-instance, are snapshot at construction time and immune to later prototype mutation.

Code example in javascript
class B { run = () => 'B'; }
const b = new B();
B.prototype.run = () => 'patched';
console.log(b.run());   // "B"

Arrow fields also bind this lexically at construction — handy for callbacks, at the cost of one closure per instance.

New.target and Abstract-ness

Inside a constructor, new.target is the constructor that was invoked with new, which enables enforced abstractness and polymorphic construction:

Code example in javascript
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new TypeError('Shape is abstract');
    }
  }
}
class Circle extends Shape {
  constructor(r) { super(); this.r = r; }
}
new Circle(2);   // fine
new Shape();     // TypeError

Because new.target is the derived constructor during subclass instantiation, base constructors can also return instances of the concrete subclass — the standard pattern before static factory methods and still useful for Proxy-based construction.

Mixins Without Multiple Inheritance

JavaScript has single inheritance. The idiomatic workaround is a function returning a class expression extending a parameter:

Code example in javascript
const Timestamped = (Base) => class extends Base {
  stampedAt = new Date();
  age() { return Date.now() - this.stampedAt.getTime(); }
};

class Record {}
class LogEntry extends Timestamped(Record) {}
console.log(new LogEntry() instanceof Record);   // true

Each Timestamped(X) call creates a fresh class, so identity checks like instanceof Timestamped are meaningless — mixins are structural in use but nominal per call. Prefer Object.assign(MyClass.prototype, mixinMethods) when you need one stable shared mixin object, at the cost of losing super and constructor hooks.

Decision Criteria

NeedReach for
Shared behavior, many instancesClass methods on the prototype
Per-instance bound callbacksArrow function fields
Truly hidden state`#` private fields, not `_`-prefixed properties or `WeakMap` side tables (unless you need cross-class or per-instance privacy semantics)
Ad-hoc compositionMixin factory or `Object.assign` on the prototype
Behavioral interception (logging, validation)`Proxy`, not class overrides, when you cannot modify the class

Prefer plain objects with functions when there is no behavior-and-identity modeling — a class adds constructor ceremony that pure data does not need.

caption: Prototype links for a class hierarchy and its instance.

alt: Diagram showing Derived extending Base, and an instance of Derived pointing to Derived.prototype via its prototype link, with method lookup flowing up to Base.prototype.

Exercise

A subclass declares an instance field initialized before calling super() in its constructor. What happens, and why?

Show answer

It throws a ReferenceError. Field initializers compile to statements that read and write this, and this is uninitialized until super() returns in a derived constructor. The correct order is: call super() first, then declare or assign fields (or place them as class fields, which run automatically after super()).

Exercise

Why can code outside a class not access a #private field even with Reflect.get and the instance in hand?

Show answer

Private fields are not properties at all. They are internal slots keyed by the class's lexical declaration and a brand check installed at construction. Reflect and property APIs operate on the property map, which never contains # fields, so no external mechanism can reach them — by design.

Exercise

new.target === Shape guards abstractness, but an attacker calls Reflect.construct(Shape, [], Circle). Does the guard hold, and what does this tell you about class privacy generally?

Show answer

It holds: Reflect.construct sets new.target to the third argument (Circle), so the guard sees Circle and allows construction — producing a Shape-branded object created as if by Circle. The broader lesson: runtime guards are conventions that the reflection layer can route around or satisfy; hard invariants need # fields or closures, not checks on public call context.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Classes

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that