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.