Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I3
20 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI3 · Part 2 · Intermediate

Prototypes & OOP

class is real syntax now — but it's still prototypes underneath, every time.

7 min read11 sections4 exercises
20 of 41

In this chapter

  • The prototype chain
  • See the lookup walk
  • What new actually does
  • class — the same four steps, with real syntax
  • Private methods, and checking for a private field

and 6 more

about 7 min
11On this page
  1. The prototype chain
  2. See the lookup walk
  3. What new actually does
  4. class — the same four steps, with real syntax
  5. Private methods, and checking for a private field
  6. Static blocks — setup that runs once, per class
  7. new.target — knowing whether new was actually used
  8. Extending a built-in, safely
  9. Object.create(null) — an object with no prototype at all
  10. Symbol.hasInstance — instanceof is not magic either
  11. Composition vs inheritance, and mixins

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.

rex (a Dog instance)
name"Rex"
breed"Labrador"
no "speak" here → check its prototype ↓
Dog.prototype
speakƒ ()
found it — lookup stops here
Object.prototype
toStringƒ ()
the chain's final stop before null
⚠ __proto__ and .prototype are not the same thing 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

See the lookup walk

Watch the lookup walk. JavaScript does not copy methods onto objects; it walks a chain until it finds one, and stops at the first hit.

Prototype chain — how a method is actually found
Lookup walk
What each level owns

Dry run: looking up dog.speak()

StepObject checkedWhat it ownsHas "speak"?
1dogname, breedno — follow [[Prototype]]
2Dog.prototypefetchno — follow [[Prototype]] again
3Animal.prototypespeakyes — lookup stops here, this stays bound to dog

Three separate objects, one method call — speak is never copied onto dog or onto Dog.prototype. Had Animal.prototype not had it either, the walk would continue to Object.prototype, then to null, and only then return undefined instead of throwing.

What new actually does

new Fn(...) is four steps, always, whether Fn is an old-style constructor function or a modern class:

  1. A brand-new, empty object is created.
  2. Its internal prototype link is set to Fn.prototype.
  3. Fn runs with this bound to that new object (the "new" row from the this-binding table).
  4. If Fn explicitly returns an object (a function counts), 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
  const returnedObject = (typeof result === "object" && result !== null) || typeof result === "function";
  return returnedObject ? 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)"
Rule Everything 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.
⚠ #private is enforced by the parser, not by convention 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.

Private methods, and checking for a private field

# isn't only for fields — a method name can start with # too, and the same parser-enforced privacy applies: it's callable only from inside the class body, on any instance of that class, not just this.

class Counter {
  #count = 0;
  #validate(n) {                      // private method — never visible outside
    if (n < 0) throw new RangeError("count can't go negative");
  }
  add(n) {
    this.#validate(n);
    this.#count += n;
    return this.#count;
  }
  static isCounter(obj) {
    return #count in obj;             // "ergonomic brand check" — true only for real instances
  }
}

const c = new Counter();
c.add(5);                              // 5
Counter.isCounter(c);                  // true
Counter.isCounter({ count: 5 });       // false — a look-alike object, not a real instance

#count in obj is the blessed way to ask "does this object actually have this private field" without triggering the TypeError that obj.#count throws on a non-instance. It's the closest JS gets to a true nominal type check — duck typing can't fake it, because a plain object can never gain a real #count slot from outside the class that declared it.

Static blocks — setup that runs once, per class

class Config {
  static #data;
  static {                                      // runs exactly once, when the class is defined
    try {
      Config.#data = JSON.parse(readConfigFile());
    } catch {
      Config.#data = {};
    }
  }
  static get(key) { return Config.#data[key]; }
}

Before static blocks, involved static setup meant either one static field with an ugly inline expression, or an init() static method every caller had to remember to invoke. A static block runs automatically, can span multiple statements, and — because it executes inside the class body — can read and write private static fields that code outside the class never could.

new.target — knowing whether new was actually used

function Widget(name) {
  if (!new.target) {
    throw new TypeError("Widget() must be called with new");
  }
  this.name = name;
}

new Widget("ok");         // fine — new.target is Widget
Widget("oops");            // throws immediately instead of silently polluting the global this

new.target is undefined when a function is called plainly, and holds the constructor actually invoked with new otherwise — including inside a subclass's super() call, where it's set to the subclass, not the base. class constructors reject a missing new automatically; new.target is the mechanism that makes that possible, and it's occasionally useful directly — an abstract-base-class pattern can throw specifically when new.target === Shape itself, while still allowing new Circle() to run Shape's constructor.

Extending a built-in, safely

class TrackedArray extends Array {
  push(...items) {
    console.log("pushing", items.length, "item(s)");
    return super.push(...items);
  }
}

const t = new TrackedArray(1, 2, 3);
t.push(4);                                        // logs, then behaves exactly like a real array
t.map((n) => n * 2) instanceof TrackedArray;        // true — array methods return the subclass
⚠ this only works because of real class semantics Extending Array (or Error, Map) with old-style function/prototype code never fully worked — the internal exotic behavior (an array's live .length, an error's .stack) can't be faked by manually wiring up a prototype chain the way the earlier myNew demo did. Real class ... extends delegates object creation to the built-in constructor itself, which is the only thing that can produce a genuinely exotic object — one of the few things class can do that the older syntax structurally could not.

Object.create(null) — an object with no prototype at all

const dict = Object.create(null);
dict.toString;                 // undefined — no Object.prototype to inherit it from
dict.hasOwnProperty;           // undefined too

const normal = {};
normal.toString;               // ƒ () — inherited from Object.prototype, as always

Every object literal implicitly inherits from Object.prototype — that's where toString, hasOwnProperty and valueOf actually come from. Object.create(null) skips that, producing a truly bare object: no inherited methods, and no risk of a data key colliding with one — a user-controlled key literally named "toString" or "__proto__" is completely inert here. That's why a hand-rolled lookup table sometimes reaches for this instead of {} — though a real Map (from the next chapter) is usually the safer default for keys you don't fully control.

Symbol.hasInstance — instanceof is not magic either

class Even {
  static [Symbol.hasInstance](value) {
    return typeof value === "number" && value % 2 === 0;
  }
}

console.log(4 instanceof Even);    // what happens?
console.log(5 instanceof Even);    // what happens?

true, then false — instanceof doesn't hard-code "walk the prototype chain"; it calls Symbol.hasInstance on the right-hand side when one exists, and only falls back to the ordinary prototype-chain walk otherwise. This is the same well-known-symbol mechanism the metaprogramming chapter covers for iteration and coercion — instanceof, for...of, and template-literal-to-string are all operators with a symbol-shaped escape hatch, not fixed language rules.

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.

Interview answer, one sentence

"Every object delegates missing properties to its prototype, forming a chain the lookup walks; new creates an object linked to the constructor's prototype, runs the constructor with this bound to it and returns it, and class is syntax over exactly that, plus genuinely private fields."

Before you move on
  • Trace a property lookup up a three-level prototype chain, naming exactly which object the search stops at and why.
  • Write the four steps new performs from memory, without looking at myNew.
  • Explain why Dog.prototype and an instance's __proto__ are related but not interchangeable.
  • Say why extending Array only works correctly with real class ... extends, not hand-wired prototypes.
  • Decide, for a concrete pair of types, whether composition or inheritance models their relationship better — and why.

04Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsextends, super, and the chainadvanced
  • 10 testsBrand a class with a private field checkintermediate
  • 8 testsWhat new actually doesintermediate
  • 9 testsYour own instanceofintermediate
All 33 for this level →
I3

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. I2Previous · I2Objects deeply
  2. I3you are here
  3. Next · I4Async, properlyPromises, done right — and the sequential-vs-parallel mistake almost everyone makes once.10 min read

01On this page

11 sections · 0%

  1. 01The prototype chain
  2. 02See the lookup walk
  3. 03What new actually does
  4. 04class — the same four steps, with real syntax
  5. 05Private methods, and checking for a private field
  6. 06Static blocks — setup that runs once, per class
  7. 07new.target — knowing whether new was actually used
  8. 08Extending a built-in, safely
  9. 09Object.create(null) — an object with no prototype at all
  10. 10Symbol.hasInstance — instanceof is not magic either
  11. 11Composition vs inheritance, and mixins

[] chapters · t top