Lesson 1 of 69

JavaScript Under the Hood

The Prototype Chain and How Inheritance Actually Works

I once failed a phone screen because I said "TypeScript classes work like Java classes." The interviewer paused, then asked me to explain what Object.getPrototypeOf returns for a class instance. I had no idea. That gap cost me the offer. This lesson closes that gap permanently.

What Is the Prototype Chain?

Every JavaScript object has a hidden internal slot called [[Prototype]]. You can read it with Object.getPrototypeOf(obj) or the legacy obj.__proto__. When you access a property on an object, the engine follows a fixed algorithm:

  1. Look for the property on the object itself.
  2. If not found, follow [[Prototype]] to the parent object.
  3. Repeat until [[Prototype]] is null.
  4. If still not found, return undefined.

This chain of linked objects is the prototype chain. There is no copying of properties. The lookup happens at runtime, every single time.

graph diagram: myDog { name: 'Rex' }

When you call myDog.toString(), the engine searches myDog, then Dog.prototype, then Animal.prototype, then Object.prototype, where it finally finds toString.

Object.create: The Raw Mechanism

Object.create(proto) creates a new object whose [[Prototype]] is set to proto. This is the most explicit way to build the chain.

Typescript
const animalProto = {
  eat(food: string): void {
    console.log(`${this.name} eats ${food}`);
  },
};

const dogProto = Object.create(animalProto) as typeof animalProto & {
  bark(): void;
};

dogProto.bark = function (): void {
  console.log(`${this.name} barks`);
};

const rex = Object.create(dogProto) as typeof dogProto & { name: string };
rex.name = "Rex";

rex.bark(); // Rex barks
rex.eat("bone"); // Rex eats bone

console.log(Object.getPrototypeOf(rex) === dogProto); // true
console.log(Object.getPrototypeOf(dogProto) === animalProto); // true

This is raw prototype manipulation. It works, but it is verbose and hard to type. That is why we have constructor functions and then the class keyword.

Constructor Functions: The Classic Pattern

Before ES6 classes, JavaScript used constructor functions. The new keyword does four things:

  1. Creates a fresh empty object.
  2. Sets its [[Prototype]] to Constructor.prototype.
  3. Calls the constructor with this pointing to the new object.
  4. Returns the object (unless the constructor explicitly returns another object).
Typescript
function Animal(this: { name: string }, name: string): void {
  this.name = name;
}

Animal.prototype.eat = function (food: string): void {
  console.log(`${this.name} eats ${food}`);
};

function Dog(this: { name: string; breed: string }, name: string, breed: string): void {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function (): void {
  console.log(`${this.name} barks`);
};

const rex = new (Dog as unknown as new (name: string, breed: string) => {
  name: string;
  breed: string;
  bark(): void;
  eat(food: string): void;
})("Rex", "Lab");

rex.bark();
rex.eat("bone");

Notice Dog.prototype = Object.create(Animal.prototype). This is how you wire up the chain manually. Forgetting Dog.prototype.constructor = Dog is a common bug that breaks instanceof.

ES6 Class Syntax: Same Thing, Better Clarity

The class keyword is syntactic sugar. At runtime, it produces the same prototype chain as the constructor function pattern above.

Typescript
class Animal {
  constructor(public name: string) {}

  eat(food: string): void {
    console.log(`${this.name} eats ${food}`);
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);
  }

  bark(): void {
    console.log(`${this.name} barks`);
  }
}

const rex = new Dog("Rex", "Lab");
rex.bark(); // Rex barks
rex.eat("bone"); // Rex eats bone

console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Dog) === Animal); // true: static side too

That last line is important. class syntax links two chains simultaneously: the instance chain (Dog.prototype -> Animal.prototype) and the static chain (Dog -> Animal). Constructor functions only give you the instance chain unless you wire the static side manually.

How instanceof Works

a instanceof B walks the prototype chain of a, looking for B.prototype. It does not check the constructor. It does not care about types.

Typescript
class Animal {}
class Dog extends Animal {}

const rex = new Dog();

console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

// You can break instanceof by replacing the prototype
const fakeDog = Object.create(Dog.prototype);
console.log(fakeDog instanceof Dog); // true: prototype matches, no constructor ran

instanceof is only reliable when you control both sides of the check. Across iframes or different module instances, instanceof fails because each environment has its own Object.prototype.

When Prototype Mutation Is Dangerous

Because the chain is shared, mutating a prototype affects every object that inherits from it, including objects already created.

Typescript
class Config {
  timeout = 5000;
}

const a = new Config();
const b = new Config();

// This mutates the shared prototype
(Config.prototype as Record<string, unknown>).timeout = 9999;

// Own properties shadow prototype properties, so a.timeout is still 5000
// because timeout is set on the instance in the constructor
console.log(a.timeout); // 5000
console.log(b.timeout); // 5000

// But adding a NEW method to the prototype does affect all instances
(Config.prototype as Record<string, unknown>).reset = function () {
  console.log("reset");
};

(a as unknown as { reset(): void }).reset(); // works: found on prototype

The real danger is when you mutate built-in prototypes like Array.prototype or Object.prototype. This is called prototype pollution. It affects every object in the entire process and is a security vulnerability in Node.js services.

Typescript
// Never do this in a library or server
(Object.prototype as Record<string, unknown>).__isAdmin = true;

const user = { name: "Alice" };
console.log((user as Record<string, unknown>).__isAdmin); // true: inherited!

Prototype pollution is a real attack vector. Attackers craft JSON payloads with __proto__ keys to inject properties into Object.prototype. Always sanitise user-supplied keys before merging objects.

What TypeScript Compiles To

When you write a TypeScript class with strict mode enabled, the TypeScript compiler emits JavaScript. In modern targets (ES2015+), it emits the class keyword directly. For older targets, it emits constructor functions. The TypeScript type system is erased completely at compile time.

Typescript
// TypeScript input
class Counter {
  private count = 0;

  increment(): void {
    this.count++;
  }

  getValue(): number {
    return this.count;
  }
}

Compiled to ES5 (to show the underlying mechanics):

Javascript
"use strict";
var Counter = /** @class */ (function () {
  function Counter() {
    this.count = 0;
  }
  Counter.prototype.increment = function () {
    this.count++;
  };
  Counter.prototype.getValue = function () {
    return this.count;
  };
  return Counter;
})();

private in TypeScript is compile-time only. At runtime, count is a plain property. Anyone can read it with instance.count in JavaScript. If you need true runtime privacy, use #count (the JavaScript private field syntax), which TypeScript also supports.

Quick Reference

ConceptWhat It Means
[[Prototype]]Hidden link to the parent object
Object.create(proto)Create object with given prototype
new Constructor()Set [[Prototype]] to Constructor.prototype, run constructor
instanceofWalk chain looking for B.prototype
class extendsSugar for wiring both instance and static prototype chains
Prototype mutationAffects all current and future instances sharing that prototype

Key insight: TypeScript classes are JavaScript prototype chains with a type-safe syntax on top. The types disappear at compile time. Understanding the chain tells you exactly what happens at runtime when you call a method, use instanceof, or accidentally mutate a shared prototype.