Prototypes & OOP
class is real syntax now — but it's still prototypes underneath, every time.
The prototype chain
Every object has an internal link to another object — its
prototype — and property lookup that doesn't find a match
walks that link outward, exactly like the scope chain walked outward
in the last chapter. The chain ends at null.
obj.__proto__ (or the modern
Object.getPrototypeOf(obj)) is the link an
instance follows. Dog.prototype is a plain
object that becomes that link for every instance
new Dog() creates. A function has a
.prototype property; an object has a
__proto__ link. They're related, never interchangeable.
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return this.name + " makes a sound"; };
const rex = new Animal("Rex");
Object.getPrototypeOf(rex) === Animal.prototype; // true
rex instanceof Animal; // true — checks exactly this chain
rex.hasOwnProperty("name"); // true — set directly on rex
rex.hasOwnProperty("speak"); // false — it's on the prototype, not rex itself
What new actually does
new Fn(...) is four steps, always, whether
Fn is an old-style constructor function or a modern
class:
- A brand-new, empty object is created.
- Its internal prototype link is set to
Fn.prototype. Fnruns withthisbound to that new object (the "new" row from the this-binding table).- If
Fnreturns an object explicitly, that's the result instead — otherwise the new object from step 1 is returned automatically.
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype); // steps 1 & 2
const result = Ctor.apply(obj, args); // step 3
return typeof result === "object" && result !== null ? result : obj; // step 4
}
function Dog(name) { this.name = name; }
Dog.prototype.speak = function () { return this.name + " barks"; };
const rex = myNew(Dog, "Rex");
console.log(rex.speak(), rex instanceof Dog); // what happens?
"Rex barks" true — a hand-rolled new that
behaves identically to the real keyword, because those are genuinely
all four steps it performs. It's worth building once, because it
turns "new is magic" into "new is Object.create plus a
function call plus a return-value check."
class — the same four steps, with real syntax
class Shape {
static count = 0; // lives on the class itself, not on instances
#id; // private field — declared up front, "#" is part of the name
constructor(name) {
this.name = name;
this.#id = ++Shape.count;
}
get id() { return this.#id; } // getters/setters, same as plain objects
describe() {
return this.name + " #" + this.id;
}
static reset() { Shape.count = 0; } // called as Shape.reset(), never on an instance
}
class Circle extends Shape {
constructor(radius) {
super("Circle"); // MUST run before "this" is usable at all
this.radius = radius;
}
describe() {
return super.describe() + " (r=" + this.radius + ")"; // extend, don't just replace
}
}
const c1 = new Circle(5);
c1.describe(); // "Circle #1 (r=5)"
class does is
still prototypes: methods land on Circle.prototype, not
on each instance, and extends just wires up the
prototype chain from the diagram above automatically.
class is real, enforced syntax on top of the exact same
machinery — not a different object model bolted on beside it.
c1.#id written outside the class body isn't a
runtime access-denied error — it's a
SyntaxError at parse time, because #id
simply isn't valid syntax anywhere the class hasn't declared it. It's
a much harder guarantee than the old _id
underscore-means-private convention, which was never actually
enforced by anything.
Composition vs inheritance, and mixins
extends models "is-a" — a Circle
is a Shape. Composition models "has-a" or
"can-do" — building an object out of smaller pieces it holds or uses,
rather than a class it descends from. Deep inheritance chains tend to
get brittle (change a base class, every descendant feels it); most
modern guidance leans composition first, inheritance only for a
genuinely stable, narrow "is-a" relationship.
A mixin is the middle ground: a function that takes a base class and returns a new one with extra behavior bolted on — reusable across classes that don't otherwise share a family tree.
const Serializable = (Base) => class extends Base {
serialize() { return JSON.stringify(this); }
};
class Point {
constructor(x, y) { this.x = x; this.y = y; }
}
class SerializablePoint extends Serializable(Point) {}
const p = new SerializablePoint(1, 2);
console.log(p.serialize()); // what happens?
{"x":1,"y":2} — Point never mentions
serialization at all. Serializable(Point) returns a
brand-new anonymous class that extends Point, so
SerializablePoint gets both its own fields and the
mixed-in method, and the same Serializable mixin could
wrap any other base class exactly the same way.
Opens in the editor — write it, run it, and check it against real tests.