JavaScript Classes, Explained

by Kuligaposten 2026-08-30

Classes in JavaScript are syntax sugar over prototypes — here is how they work, and where the sugar leaks.

JavaScript Classes, Explained

A class in JavaScript is mostly a nicer way to write what prototypes were already doing. Under the hood, methods still end up on the prototype object, and instances are still created by prototype chains — class just gives that a familiar, readable syntax.

The minimal example

class Counter {
  constructor(start = 0) {
    this.count = start;
  }

  increment() {
    this.count += 1;
    return this.count;
  }
}

const counter = new Counter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

constructor runs once, when new Counter() is called. increment isn't copied onto every instance — it lives on Counter.prototype, and every instance shares the same function object.

console.log(counter.increment === Counter.prototype.increment); // true

Fields, private fields, and methods

Modern JavaScript lets you declare instance fields directly in the class body, and mark some of them truly private with a # prefix:

class BankAccount {
  #balance;

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

  deposit(amount) {
    this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Insufficient funds");
    this.#balance -= amount;
    return this.#balance;
  }

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

const account = new BankAccount(100);
account.deposit(50);
console.log(account.balance); // 150
console.log(account.#balance); // SyntaxError: outside the class body

Unlike a closure-based private variable, #balance is a real language feature — it's not accessible or even enumerable from outside the class, and it doesn't rely on convention like _balance does.

Inheritance with extends and super

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

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

class Dog extends Animal {
  speak() {
    return `${super.speak()} Specifically, it barks.`;
  }
}

const dog = new Dog("Rex");
console.log(dog.speak()); // "Rex makes a sound. Specifically, it barks."

extends wires up the prototype chain so Dog.prototype inherits from Animal.prototype. super.speak() calls the parent's version of the method explicitly — without it, Dog's speak would just override Animal's with no way to reuse it.

If a subclass defines its own constructor, it must call super(...) before touching this:

class Cat extends Animal {
  constructor(name, indoor) {
    super(name); // must run first
    this.indoor = indoor;
  }
}

Static members

static attaches a property or method to the class itself, not to instances:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static origin() {
    return new Point(0, 0);
  }

  static distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }
}

const p1 = Point.origin();
const p2 = new Point(3, 4);
console.log(Point.distance(p1, p2)); // 5

Point.origin and Point.distance are useful without ever creating an instance first — they're organized under the class as a namespace.

The gotcha: this isn't automatically bound

Class methods lose their this binding once detached from the instance, just like ordinary functions:

class Button {
  constructor(label) {
    this.label = label;
  }

  handleClick() {
    console.log(`Clicked: ${this.label}`);
  }
}

const button = new Button("Submit");
const handler = button.handleClick;
handler(); // TypeError: Cannot read properties of undefined

handler() is called with no receiver, so this is undefined inside (strict mode is on by default in classes). Two common fixes:

class Button {
  constructor(label) {
    this.label = label;
    this.handleClick = this.handleClick.bind(this); // bind in constructor
  }

  handleClick() {
    console.log(`Clicked: ${this.label}`);
  }
}
class Button {
  constructor(label) {
    this.label = label;
  }

  // arrow field: captures `this` from the constructor, no bind() needed
  handleClick = () => {
    console.log(`Clicked: ${this.label}`);
  };
}

The arrow-field version creates one function per instance instead of sharing it on the prototype — a small memory tradeoff for not having to remember to bind.

Classes are still just functions

console.log(typeof Counter); // "function"

Calling a class without new throws (TypeError: Class constructor Counter cannot be invoked without 'new'), and class bodies are always implicitly strict — those are the real differences from an old-style constructor function. Everything else — the prototype chain, shared methods, instanceof — works exactly the same as it always did.

Back to Home