Engineering Notes
Home/Languages/JavaScript/Classes
Languages · JavaScript

Classes

A class in JavaScript compiles down to the exact same constructor-function-plus-prototype mechanism from the Prototypes topic. It does not introduce a new object model — it introduces cleaner syntax for one that already existed.

💻 Languages● Beginner⏱ 6 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Classes Are Syntactic Sugar Over Prototypes
  • Anatomy of a Class
  • #private Fields Are Real Privacy
  • Inheritance with extends and super
  • Class Methods Are Not Hoisted Like Function Declarations

1Classes Are Syntactic Sugar Over Prototypes

A class in JavaScript compiles down to the exact same constructor-function-plus-prototype mechanism from the Prototypes topic. It does not introduce a new object model — it introduces cleaner syntax for one that already existed.

js
class Dog {
  constructor(name) {
    this.name = name;
  }
  bark() {
    return `${this.name} says woof`;
  }
}

// Equivalent, pre-class syntax:
function DogFn(name) { this.name = name; }
DogFn.prototype.bark = function () { return `${this.name} says woof`; };

bark lives on Dog.prototype, shared by every instance — identical to the manual prototype version.

2Anatomy of a Class

js
class BankAccount {
  static bankName = "JS National Bank"; // shared across ALL instances, not per-instance
  #balance;                              // private field — only accessible inside this class

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

  deposit(amount) {                      // instance method — on the prototype
    this.#balance += amount;
    return this.#balance;
  }

  get balance() {                        // getter — accessed like a property, not called
    return this.#balance;
  }

  static create(owner) {                 // static method — called on the class, not instances
    return new BankAccount(owner);
  }
}
FeatureSyntaxLives onShared or per-instance
Instance fieldfield = value;instanceper-instance
Private field#field;instanceper-instance, truly inaccessible outside
Instance methodmethod() {}prototypeshared
Static field/methodstatic x = ...the class itselfshared, one copy total
Getter/setterget x() {} / set x(v) {}prototypeshared, accessed as a property

3#private Fields Are Real Privacy

Unlike closures-based privacy, #field access outside the class is a syntax error, not just convention:

js
class Secret {
  #value = 42;
  reveal() { return this.#value; }
}
const s = new Secret();
console.log(s.reveal()); // 42
console.log(s.#value);   // SyntaxError — # fields aren't just "hidden", they're inaccessible syntax

4Inheritance with extends and super

js
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);           // MUST call super() before using `this` in a derived constructor
    this.breed = breed;
  }
  speak() {                // overrides Animal's speak
    return `${super.speak()}, specifically a bark`; // super.method() calls the parent's version
  }
}

const rex = new Dog("Rex", "Labrador");
console.log(rex.speak()); // "Rex makes a sound, specifically a bark"
console.log(rex instanceof Animal); // true

extends links Dog.prototype's prototype to Animal.prototype — extending the prototype chain by one link, exactly as in the Prototype Chain topic.

rex links to Dog.prototype, which links to Animal.prototype via extends
rex links to Dog.prototype, which links to Animal.prototype via extends

5Class Methods Are Not Hoisted Like Function Declarations

js
const early = new Foo(); // ReferenceError — classes are NOT hoisted the way function declarations are
class Foo {}

Classes are hoisted (their name is reserved) but remain in the Temporal Dead Zone until the declaration line runs — same rule as let/const.

Under the HoodField Order and Hidden Class Stability▾

Everything from the Prototypes topic's "Under the Hood" section (hidden classes, transition trees, inline caches) applies directly to class instances — class is syntax for exactly that machinery, so writing a class well or poorly has a direct, measurable performance consequence, not just a stylistic one.

Why declaring fields in a fixed order matters

js
class PointGood {
  x; y; // declared as class fields, in a FIXED order, before the constructor even runs
  constructor(x, y) { this.x = x; this.y = y; }
}

class PointBad {
  constructor(x, y, includeZ) {
    this.x = x;
    this.y = y;
    if (includeZ) this.z = 0; // conditionally adds a property — only SOME instances get it
  }
}

Every PointGood instance transitions through the exact same hidden-class sequence — {} → {x} → {x,y} — because the fields are always added in the same order. Every PointBad instance created with includeZ = true ends up on a different hidden class than one created with includeZ = false, even though they're the same class. Call sites that operate on both end up polymorphic (or worse) instead of monomorphic, exactly as described in the Prototype Chain topic — the fix is declaring all fields up front (as class fields, or unconditionally in the constructor) so every instance follows one predictable transition path.

#private fields are not just "hidden" — they're a different storage mechanism entirely

Public fields participate in the ordinary hidden-class/property system described above. #private fields are specified and implemented as a separate mechanism — conceptually closer to a WeakMap keyed by instance (though real engines optimize this further) than to a regular own property. This is why #value can't be reached via Object.keys, bracket notation, Reflect.ownKeys, or any of the normal property-introspection tools from the Objects topic — it was never a property of the object in the same sense x and y are; it's stored through an entirely separate, syntax-gated channel that only code lexically inside the class body can address at all.

Methods are added to the prototype exactly once, ever

js
class Dog {
  bark() { return "woof"; } // this function object is created ONCE, when the class is defined
}

Unlike a field initializer (which runs per-instance, in the constructor), bark is attached to Dog.prototype a single time, when the class declaration itself is evaluated — every instance shares the literal same function object, which is both why it's memory-efficient (see the Prototypes topic) and why mutating Dog.prototype.bark after the fact affects every existing and future instance simultaneously.

⚠️ Common Pitfalls

  1. Forgetting super() in a derived class constructor — accessing this before calling super() throws a ReferenceError. In a derived class, this doesn't exist until super() runs.
  2. Class methods are not auto-bound — detaching a method (const fn = instance.method) loses this, exactly like regular prototype methods (see the this keyword topic). Use arrow class fields if you need auto-binding.
  3. Static members are not inherited to instances — BankAccount.bankName works; accountInstance.bankName does not, unless accessed via this.constructor.bankName.
  4. Class bodies are always strict mode — this in a detached method call is undefined, never the global object, regardless of the surrounding file's mode.
  5. Forgetting classes aren't hoisted like functions — you cannot instantiate a class before its declaration line, even though the name is reserved earlier.

📋 Quick Reference

text
class X extends Y {
  static s = ...       // one per class
  #private = ...        // one per instance, truly private
  constructor() { super(); }  // must call super() first if extending
  method() {}            // shared via X.prototype
  get/set prop() {}       // accessed as a property
}

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What features do JavaScript classes have?core▾

Constructors, methods on the prototype, static members, getters/setters, inheritance with extends/super, public and private fields (#count), static blocks, and private methods.

Follow-ups interviewers ask
  • Are #private fields truly private? Yes, enforced by the language, unlike _convention.
Q2Why must you call super() before using this in a subclass constructor?core▾

The parent constructor creates/initializes the object; until super() runs, this isn't initialized and accessing it throws.

Follow-ups interviewers ask
  • When can you omit the constructor? If it only calls super with the same args.
Q3Composition vs inheritance in JavaScript?senior▾

Prefer composition (objects containing or delegating to others, functions, mixins) for flexibility; use inheritance for clear is-a hierarchies with shared behaviour. Deep hierarchies become brittle.

Follow-ups interviewers ask
  • Mixin pattern? Functions that take a class and return an extended class.
Q4What's the difference between static and instance members?core▾

Static members belong to the class itself (Math.max, factory methods, caches); instance members belong to each object.

Follow-ups interviewers ask
  • Can static methods access instance fields? Not without an instance.
Q5How do getters and setters help?core▾

They expose computed or validated properties with property syntax (circle.area), letting you add logic without changing the API.

Follow-ups interviewers ask
  • Pitfall? Expensive getters look like cheap property reads.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.