Prototypes, Classes & Inheritance
JavaScript's OOP isn't class-based underneath — it's prototype-based. class is real syntax that compiles down to the same prototype chain you'd build by hand.
The Prototype Chain
Every object has an internal link to another object, its prototype. When you access a property JS doesn't find on the object itself, it walks up the chain until it finds it or hits null. This is how methods are shared without copying them onto every instance.
function Animal(name) { this.name = name; } Animal.prototype.speak = function() { return `${this.name} makes a sound`; }; const dog = new Animal("Rex"); dog.speak(); // "Rex makes a sound" — found via the prototype dog.hasOwnProperty("name"); // true — set directly on the instance dog.hasOwnProperty("speak"); // false — lives on Animal.prototype Object.getPrototypeOf(dog) === Animal.prototype; // true
Classes
class (ES2015) is cleaner syntax over the exact same prototype mechanism above — no new capability, just a familiar shape for people coming from class-based languages.
class Animal { constructor(name) { this.name = name; } speak() { // still added to Animal.prototype under the hood return `${this.name} makes a sound`; } } const cat = new Animal("Whiskers"); cat.speak(); // "Whiskers makes a sound" typeof Animal; // "function" — classes are still functions
Encapsulation — Private Fields
Before ES2022, "private" was a naming convention (_balance) that JS never enforced. The # prefix now creates a field that's genuinely inaccessible from outside the class.
class BankAccount { #balance = 0; // truly private — not just convention deposit(amount) { if (amount <= 0) throw new Error("Invalid amount"); this.#balance += amount; return this.#balance; } get balance() { return this.#balance; } // controlled read access } const acc = new BankAccount(); acc.deposit(100); acc.balance; // 100 — via the getter // acc.#balance; // SyntaxError — inaccessible outside the class
Inheritance
class Dog extends Animal { constructor(name, breed) { super(name); // must run before using `this` this.breed = breed; } speak() { // override return super.speak() + " — specifically, barks"; } } const rex = new Dog("Rex", "Lab"); rex.speak(); // "Rex makes a sound — specifically, barks" rex instanceof Animal; // true
extends. For sharing behavior across unrelated classes, use mixins (a function that returns a class extending its argument) instead.Polymorphism
Because JS is dynamically typed, polymorphism needs no interface or annotation — any object with a matching method shape (duck typing) works, related by inheritance or not.
class Cat { speak() { return "Meow"; } } class Robot { speak() { return "BEEP"; } } // unrelated to Animal function makeItTalk(thing) { return thing.speak(); // works for ANY object with a .speak() method } [new Dog("Rex"), new Cat(), new Robot()].map(makeItTalk); // each calls its own version of speak() — no shared base class required
Static Members & Getters/Setters
class Circle { static unit = new Circle(1); // belongs to the class, not instances constructor(radius) { this._radius = radius; } static fromDiameter(d) { // factory — Circle.fromDiameter(10) return new Circle(d / 2); } get area() { return Math.PI * this._radius ** 2; } set radius(r) { if (r < 0) throw new Error("radius can't be negative"); this._radius = r; } } const c = Circle.fromDiameter(10); c.area; // reads like a property, runs like a method c.radius = 7; // runs the setter's validation