Classes in JavaScript — syntax sugar over prototypes, with real behavior
Classes are built on the prototype mechanism covered earlier in this domain, but they add real, enforced behaviors — a genuine private-field syntax, and a strict rule about calling super() — that plain prototype code never had on its own.
3 min read
Private fields: genuine enforcement, not just a naming convention
class BankAccount {
#balance = 0; // the # prefix makes this a REAL private field — enforced by the engine, not convention
deposit(amount) { this.#balance += amount; }
getBalance() { return this.#balance; }
}
const account = new BankAccount();
account.deposit(100);
account.getBalance(); // 100
account.#balance; // SyntaxError — not just "undefined," a genuine parse-time errorBefore private fields, JavaScript's closest approximation to "private" data was a closure (covered in this domain's scope-and-closures lesson) or simply a naming convention like _balance (an underscore prefix meaning "please don't touch this," entirely unenforced). #balance is different: accessing #balance from outside the class is a genuine SyntaxError, not a runtime undefined — the privacy is enforced by the language itself, at parse time, not merely a social contract between developers.
super() must be called before this is used at all, in a subclass constructor
class Animal {
constructor(name) { this.name = name; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST come before any use of `this` below
this.breed = breed;
}
}
class Broken extends Animal {
constructor(name) {
this.breed = "unknown"; // ReferenceError — `this` used before super()
super(name);
}
}In a subclass, this genuinely doesn't exist yet until super() (the parent class's constructor) has run — a real, enforced rule, not a style guideline. This mirrors the earlier prototype lesson's structural point: extends sets up the prototype chain, and super() is what actually initializes the parent's portion of the new object, which has to happen before the subclass can add anything of its own to this.
Static methods and fields: attached to the class itself, not instances
class User {
static #nextId = 1; // shared across ALL instances — belongs to the class, not any one user
#id;
constructor(name) {
this.name = name;
this.#id = User.#nextId++;
}
static createGuest() { return new User("Guest"); } // called as User.createGuest(), not on an instance
}
User.createGuest().name; // "Guest"static members belong to the class itself, not to any individual instance — User.#nextId is a single, shared counter across every User ever created, and static createGuest() is called directly as User.createGuest(), never on an instance (new User().createGuest() would fail — static methods simply aren't on instances at all). This is the idiomatic place for genuinely class-level concerns: shared counters, factory methods that construct instances, and utility functions logically tied to the class but not to any one object's state.
Getters and setters: methods that look like property access
class Temperature {
#celsius;
constructor(celsius) { this.#celsius = celsius; }
get fahrenheit() { return this.#celsius * 9/5 + 32; } // called as a PROPERTY, not a method
set fahrenheit(f) { this.#celsius = (f - 32) * 5/9; }
}
const temp = new Temperature(20);
temp.fahrenheit; // 68 — reads like a property, but genuinely RUNS a computation
temp.fahrenheit = 100; // WRITES through the setter, converting and storing as celsiusA get/set pair lets a class expose something that reads and writes like a plain property (temp.fahrenheit, no parentheses) while actually running real logic underneath — computing a derived value on read, validating or transforming on write. This is a genuine, real mechanism (not cosmetic) for controlling access to internal state while keeping the external API looking like simple property access, rather than forcing every caller to write temp.getFahrenheit().
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. How is a `#balance` private field different from the old `_balance` naming-convention approach?
2. Why must `super()` be called before using `this` in a subclass constructor?
3. What's the difference between a `static` method and an instance method?