02 · Prototypes & Classes¶
Every object has a prototype¶
JavaScript objects link to another object called their prototype, and look
up missing properties there. This chain is how methods like .toString()
"just work" on objects you never defined them on.
const animal = {
eats: true,
describe() {
return "an animal that eats";
},
};
const rabbit = Object.create(animal); // rabbit's prototype is `animal`
rabbit.jumps = true;
console.log(rabbit.jumps); // true — own property
console.log(rabbit.eats); // true — found on the prototype
console.log(rabbit.describe()); // an animal that eats — inherited method
console.log(Object.getPrototypeOf(rabbit) === animal); // true
Constructor functions (the pre-class way)¶
Before class syntax, functions combined with new and .prototype were
the standard way to create many objects that share methods.
function Person(name, age) {
this.name = name;
this.age = age;
}
// Methods go on the prototype so every instance shares ONE copy in memory
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const ada = new Person("Ada", 30);
const grace = new Person("Grace", 45);
console.log(ada.greet()); // Hi, I'm Ada
console.log(grace.greet()); // Hi, I'm Grace
console.log(ada.greet === grace.greet); // true — same function, shared via prototype
class syntax¶
class is syntactic sugar over the same prototype mechanism — cleaner to
read, but it compiles down to functions and prototypes under the hood.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
const dog = new Animal("Rex");
console.log(dog.speak()); // Rex makes a sound.
console.log(dog instanceof Animal); // true
console.log(typeof Animal); // function — classes are functions under the hood
Inheritance with extends and super¶
extends sets up the prototype chain between two classes; super calls the
parent class's constructor or methods.
class Dog extends Animal {
constructor(name, breed) {
super(name); // must run before using `this` — calls Animal's constructor
this.breed = breed;
}
speak() {
const base = super.speak(); // call the parent version, then extend it
return `${base} Specifically, a bark! (${this.breed})`;
}
}
const rex = new Dog("Rex", "Labrador");
console.log(rex.speak());
// Rex makes a sound. Specifically, a bark! (Labrador)
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true — Dog's chain includes Animal
Getters, setters, and private fields¶
Classes support computed properties via get/set, and true private state
using a # prefix (not accessible from outside the class at all).
class BankAccount {
#balance; // private field — a syntax error to access as account.#balance outside
constructor(initialBalance = 0) {
this.#balance = initialBalance;
}
get balance() {
return this.#balance;
}
deposit(amount) {
if (amount <= 0) throw new Error("deposit must be positive");
this.#balance += amount;
}
}
const account = new BankAccount(100);
account.deposit(50);
console.log(account.balance); // 150 — read through the getter
// console.log(account.#balance); // SyntaxError if written outside the class
Static members¶
static properties and methods belong to the class itself, not to
instances — useful for factory methods or shared utilities.
class Point {
static origin = new Point(0, 0); // shared "constant" instance
constructor(x, y) {
this.x = x;
this.y = y;
}
static distance(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
}
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
console.log(Point.distance(p1, p2)); // 5
console.log(Point.origin.x, Point.origin.y); // 0 0
Prototype chain vs. class syntax¶
| Concept | Constructor function style | class style |
|---|---|---|
| Create instance | new Person(...) |
new Person(...) |
| Shared methods | Person.prototype.method = ... |
defined inside the class body |
| Inheritance | manual Object.create/.call() wiring |
extends + super |
| Private state | closures or naming conventions (_balance) |
#field (enforced by the engine) |
| Underlying mechanism | prototype chain | same prototype chain, nicer syntax |
How It Actually Works¶
class syntax is sugar over V8's actual object model: every object carries an internal
[[Prototype]] slot (exposed as __proto__ or via Object.getPrototypeOf), and
property lookup that misses on the object itself walks up this prototype chain one
link at a time until it hits a match or reaches null. This lookup isn't a generic
walk on every call, though — V8's inline caches record the full prototype chain shape
for a given access site, so repeated lookups of an inherited method resolve in
essentially the same constant time as an own property, as long as the chain's shape
stays stable.
Methods defined inside a class body are non-enumerable by design (unlike properties
you assign with this.x =), and they live once on ClassName.prototype, shared by
every instance — this is why methods don't duplicate per-instance the way closures over
constructor-scope variables would. class also enforces things prototype-based
constructors never did: calling a class without new throws immediately, and a
subclass constructor cannot reference this until it calls super() — because the
engine hasn't allocated this yet; super() is literally the call that runs the
parent constructor and produces the object this will refer to. Private fields
(#field) aren't just naming convention — they're stored in a separate internal slot
keyed by a per-class unique brand, so external code can't even detect their existence via
Object.keys or bracket access, unlike _field which is just a public property with an
underscore.
Exercise¶
Create a Shape base class with a constructor field name and a method
describe() returning "<name> has an area of <area>". Then create
Rectangle and Circle subclasses that extend Shape, each implementing
its own get area() getter (Rectangle: width * height; Circle:
Math.PI * radius ** 2, rounded to 2 decimals). Verify describe() works
correctly for both.