Classes are sugar — but very useful sugar
The class keyword arrived in ES2015 and has been steadily extended ever since. Under the hood it is still the prototype model from the previous post — but the syntax is cleaner, the semantics are stricter, and modern additions (private fields, static blocks) genuinely solve real problems.
Foundations: Objects — the basics, Prototypes and the chain.
A complete class at a glance
1class Account {
2 #balance; // private field, brand-checked
3
4 static minimumDeposit = 1; // static (class-level) field
5
6 constructor(owner, opening = 0) {
7 this.owner = owner;
8 this.#balance = opening;
9 }
10
11 // Instance method — lives on Account.prototype
12 deposit(amount) {
13 if (amount < Account.minimumDeposit) throw new RangeError("too small");
14 this.#balance += amount;
15 return this;
16 }
17
18 // Getter — accessed without ()
19 get balance() {
20 return this.#balance;
21 }
22
23 // Static method — called on the class itself
24 static from(plain) {
25 return new Account(plain.owner, plain.balance);
26 }
27}
28
29const a = Account.from({ owner: "Ali", balance: 100 });
30a.deposit(20).deposit(5);
31a.balance; // 125
That single example uses every feature this post covers.
Constructors and instance fields
The constructor runs when new ClassName(...) is called. It is the right place to assign instance state.
Class-field syntax (ES2022) lets you declare instance properties outside the constructor — handy when the default value does not depend on arguments:
1class Counter {
2 count = 0; // class field — initialised on every instance
3 step = 1;
4
5 inc() { this.count += this.step; }
6}
Class fields are defined, not assigned — which means they always create an own property and never trigger inherited setters.
Methods live on the prototype
1class Dog { bark() { return "woof"; } }
2const rex = new Dog();
3
4rex.bark === Dog.prototype.bark; // true
5Object.getPrototypeOf(rex) === Dog.prototype; // true
This is why adding a method to a class affects every existing instance — instances do not own the method; they look it up on Dog.prototype each call.
Getters and setters
Computed properties that look like fields from the outside:
1class Temperature {
2 constructor(celsius) { this._c = celsius; }
3
4 get fahrenheit() { return this._c * 9 / 5 + 32; }
5 set fahrenheit(f) { this._c = (f - 32) * 5 / 9; }
6}
7
8const t = new Temperature(20);
9t.fahrenheit; // 68
10t.fahrenheit = 100;
11t._c; // ~37.8
Getters and setters are the cleanest way to add validation or derived values without changing the call sites.
Static members
static puts a property on the class itself, not on instances. Use it for factories, constants, and utility methods that conceptually belong to the type:
1class Point {
2 constructor(x, y) { this.x = x; this.y = y; }
3
4 static origin = new Point(0, 0);
5
6 static distance(a, b) {
7 return Math.hypot(a.x - b.x, a.y - b.y);
8 }
9
10 static {
11 // Static initialisation block (ES2022) — runs once when the class is defined.
12 // Perfect for set-up that needs multiple statements.
13 console.log("Point class loaded");
14 }
15}
16
17Point.distance(Point.origin, new Point(3, 4)); // 5
Private members
Prefix with # to make a field, method, getter, setter, or static member truly private. The privacy is enforced by the engine — no Reflect, no Object.keys, no debugger trick can reach it from outside the class body.
1class PinPad {
2 #pin;
3
4 constructor(pin) { this.#pin = pin; }
5
6 #hash(s) { return s.split("").reverse().join(""); } // private method
7
8 check(input) {
9 return this.#hash(input) === this.#hash(this.#pin);
10 }
11}
12
13const p = new PinPad("1234");
14p.check("1234"); // true
15p.#pin; // SyntaxError at parse time
Private members entirely replace the old _underscore convention. They also make #field in obj a fast brand check — true only for instances of the class that declared #field.
Inheritance with extends and super
1class Animal {
2 constructor(name) { this.name = name; }
3 speak() { return `${this.name} makes a sound`; }
4}
5
6class Dog extends Animal {
7 constructor(name, breed) {
8 super(name); // MUST be called before using `this`
9 this.breed = breed;
10 }
11
12 speak() {
13 return `${super.speak()} — specifically, a bark`;
14 }
15}
16
17const rex = new Dog("Rex", "Labrador");
18rex.speak(); // "Rex makes a sound — specifically, a bark"
extendslinks the child’s prototype to the parent’s prototype.super(...)invokes the parent constructor; required beforethisis accessible.super.method()calls the parent’s version, useful for extending instead of replacing.
Extending built-ins
You can extend Array, extends Error, extends Map, etc.:
1class CaseInsensitiveSet extends Set {
2 add(value) { return super.add(String(value).toLowerCase()); }
3 has(value) { return super.has(String(value).toLowerCase()); }
4}
Important
When extending
Error, always setthis.name = this.constructor.namein the constructor so stack traces show the subclass name. Modern Node and browsers handleError.captureStackTrace(Node) or the V8 capture call for you automatically when youextends Error.
instanceof and brand checks
1const rex = new Dog("Rex");
2rex instanceof Dog; // true
3rex instanceof Animal; // true
4rex instanceof Object; // true
instanceof walks the prototype chain, exactly as covered in the previous post.
For private fields, the in operator is the modern, optimised brand check:
1class Account { #balance; static is(x) { return #balance in x; } }
2Account.is(new Account()); // true
3Account.is({}); // false
Composition vs inheritance
Deep inheritance hierarchies age badly. Where possible, prefer composition — give an object what it needs by combining smaller objects:
1class Cart {
2 constructor(taxCalculator, shippingCalculator) {
3 this.tax = taxCalculator;
4 this.shipping = shippingCalculator;
5 }
6 total(items) {
7 const subtotal = items.reduce((s, it) => s + it.price, 0);
8 return subtotal + this.tax.for(subtotal) + this.shipping.for(items);
9 }
10}
Each helper can be swapped or tested in isolation, and the Cart class never grows a tangle of subclasses.
Summary
classis sugar over prototypes — useful, strict sugar with real new features.- Use class fields for default state,
#fieldsfor true privacy, getters/setters for derived properties. staticfor class-level members;static { }blocks for multi-line initialisation.extends+super(...)for inheritance; always callsuperbeforethisin the constructor.- Prefer composition over deep inheritance trees.
The series ends with Modern object features — descriptors, freeze/seal, symbols, iterators, and structured cloning.










