Engineering Notes
Home/Languages/JavaScript/Prototypes
Languages · JavaScript

Prototypes

Every JavaScript object has an internal link to another object called its prototype. When you access a property that doesn't exist directly on an object, the engine automatically looks it up on the prototype — and if it's not there either, on the prototype's prototype, and so on.

💻 Languages● Beginner⏱ 7 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What a Prototype Is
  • __proto__ vs prototype — Two Different Things
  • Own Properties vs Inherited Properties
  • Setting a Prototype Property Never Mutates the Prototype
  • Object.create vs Constructor Functions vs class
Part 1The Mechanism

1What a Prototype Is

Every JavaScript object has an internal link to another object called its prototype. When you access a property that doesn't exist directly on an object, the engine automatically looks it up on the prototype — and if it's not there either, on the prototype's prototype, and so on.

js
const animal = {
  eats: true,
  describe() { return "I am an animal"; },
};

const dog = Object.create(animal); // dog's prototype = animal
dog.barks = true;

console.log(dog.eats);       // true — found on the prototype
console.log(dog.describe()); // "I am an animal" — inherited method
console.log(dog.barks);      // true — own property

dog doesn't have eats or describe as its own properties — Object.create(animal) sets animal as dog's prototype, and lookups fall through to it.

2__proto__ vs prototype — Two Different Things

This is the single most confusing naming collision in JavaScript:

NameWhat it isExists on
obj.__proto__ (or Object.getPrototypeOf(obj))The actual link to the object's prototypeevery object
Fn.prototypeThe object that becomes the prototype of instances created with new Fn()every function
js
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return `${this.name} says woof`; };

const rex = new Dog("Rex");
console.log(rex.__proto__ === Dog.prototype); // true
console.log(rex.bark());                       // "Rex says woof"

Dog.prototype is a plain object. new Dog(...) creates a new object whose __proto__ points at Dog.prototype. That's the entire mechanism behind "constructor functions."

rex.__proto__ points at Dog.prototype, which is a separate object from the Dog function itself
rex.__proto__ points at Dog.prototype, which is a separate object from the Dog function itself

3Own Properties vs Inherited Properties

js
console.log(rex.hasOwnProperty("name")); // true — set in the constructor
console.log(rex.hasOwnProperty("bark")); // false — inherited via the prototype
console.log("bark" in rex);              // true — `in` checks the whole chain

for...in loops over both own and inherited enumerable properties, which is a common source of bugs — prefer Object.keys(obj) (own, enumerable only) when you just want the object's own data.

4Setting a Prototype Property Never Mutates the Prototype

js
dog.eats = false; // creates a NEW own property on dog
console.log(dog.eats);       // false
console.log(animal.eats);    // true — untouched

Assignment always creates/updates an own property; it never writes through to the prototype. Only reading falls through the chain — writing does not.

5Object.create vs Constructor Functions vs class

All three ultimately do the same thing — link one object's prototype to another:

js
// 1. Object.create — direct, explicit
const catA = Object.create(animal);

// 2. Constructor function — prototype set via Fn.prototype
function Cat() {}
Cat.prototype = animal;
const catB = new Cat();

// 3. class — syntactic sugar over the same mechanism
class Bird {}
Object.setPrototypeOf(Bird.prototype, animal);
const catC = new Bird();

class (covered in its own topic) is the modern, readable way to do this — but it compiles down to exactly this prototype machinery.

Under the HoodHidden Classes and Inline Caches▾

If every obj.property access literally walked the prototype chain object-by-object every single time, property access would be far too slow for real-world code. Engines avoid this with two cooperating optimizations.

Hidden classes: giving objects a "shape"

V8 (and equivalent engines) assign every object an internal hidden class (sometimes called a "shape" or "structure") describing its layout: which properties it has, in which order, at which memory offsets. Two objects created the same way, with properties added in the same order, share the same hidden class:

js
function Point(x, y) { this.x = x; this.y = y; }
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// p1 and p2 share the SAME hidden class — both have {x, y} added in the same order

Adding a property transitions an object from one hidden class to another, forming a transition tree — this is precisely why constructor functions (and class, see the Classes topic) that always initialize the same fields in the same order are fast: every instance follows the identical transition path and ends up sharing hidden classes, instead of each object growing its own unique, unshared layout.

js
const p3 = new Point(5, 6);
p3.z = 7; // p3 now transitions to a DIFFERENT hidden class than p1/p2 — it has an extra property

Inline caches: remembering where a property was found last time

At each call site that reads a property (e.g. one specific obj.eats expression in your code), the engine keeps an inline cache (IC) that remembers: "the last time I saw an object with hidden class X here, the property was at offset N." The next time that same call site runs, if the incoming object has the same hidden class, the engine skips the prototype-chain walk entirely and jumps straight to that remembered offset.

IC stateMeaningSpeed
Monomorphicthis call site has only ever seen ONE hidden classfastest — direct offset read, no chain walk
Polymorphicit's seen a handful (2-4) of different hidden classesstill fast — small lookup table of cached offsets
Megamorphicit's seen many different hidden classesfalls back to a slower, generic property lookup — the chain-walking algorithm from the Prototype Chain topic runs for real again

This is a direct, practical consequence of everything in sections 1-5: dog.eats looks like a single operation, but its actual speed depends heavily on whether the objects flowing through that line of code consistently share a hidden class — which in turn depends on using consistent constructors/classes rather than ad-hoc, differently-shaped object literals.

Part 2Shared Methods and Practical Patterns

1Why Prototypes Save Memory

Methods placed on a shared prototype exist once in memory, no matter how many instances are created — unlike defining a method inside the constructor, which creates a new function object per instance.

js
// BAD: new function created for every single Dog instance
function DogWasteful(name) {
  this.name = name;
  this.bark = function () { return `${this.name} says woof`; }; // duplicated per instance
}

// GOOD: one shared function on the prototype
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return `${this.name} says woof`; };

const dogs = Array.from({ length: 10000 }, (_, i) => new Dog(`dog${i}`));
// Wasteful version: 10000 separate bark function objects
// Prototype version: 1 shared bark function, referenced by all 10000

2The Prototype Chain Terminates at null

js
console.log(Object.getPrototypeOf({}));                // Object.prototype
console.log(Object.getPrototypeOf(Object.prototype));   // null — the end of the chain

Every plain object's chain eventually reaches Object.prototype (which provides toString, hasOwnProperty, etc.), then null.

js
const bare = Object.create(null); // no prototype at all — not even Object.prototype
console.log(bare.toString); // undefined — no inherited methods whatsoever

Object.create(null) is used for objects meant to be pure key-value dictionaries, immune to prototype pollution attacks.

3Overriding an Inherited Method

js
function Animal() {}
Animal.prototype.speak = function () { return "..."; };

function Cat() {}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.speak = function () { return "Meow"; }; // shadows Animal's version

const c = new Cat();
console.log(c.speak()); // "Meow"

This is exactly how method overriding works in class-based inheritance too — class syntax just automates this wiring.

4Checking the Chain

js
console.log(rex instanceof Dog);      // true — Dog.prototype is somewhere in rex's chain
console.log(Dog.prototype.isPrototypeOf(rex)); // true, equivalent check
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true, direct link only

instanceof walks the entire chain; Object.getPrototypeOf only gives you the immediate link.

⚠️ Common Pitfalls

  1. Mutating a shared prototype object accidentally — Dog.prototype.tricks = [] creates ONE array shared by every Dog instance, not a per-instance array. Always initialize per-instance state (arrays, objects) inside the constructor.
  2. Confusing Fn.prototype with Object.getPrototypeOf(instance) — they refer to the same object, but from different sides of the relationship; mixing them up is extremely common when debugging.
  3. Using for...in without hasOwnProperty checks — iterates inherited enumerable properties too, which can pull in unexpected keys if the prototype has been extended.
  4. Long prototype chains hurting lookup performance — every miss walks one more link; deeply nested inheritance (5+ levels) measurably slows property access in hot code paths.
  5. Reassigning Fn.prototype entirely after instances already exist — existing instances keep their old prototype link; only instances created after the reassignment get the new one.

📋 Quick Reference

text
instance.__proto__  ===  Constructor.prototype
instance.hasOwnProperty(key)   → true only for own properties
key in instance                → true for own OR inherited
Object.keys(instance)          → own enumerable keys only
for...in instance               → own AND inherited enumerable keys

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What is a prototype in JavaScript?core▾

Every object has an internal link ([[Prototype]]) to another object; property lookups that fail on the object continue up this link. Constructors' prototype property becomes the prototype of instances created with new.

Follow-ups interviewers ask
  • __proto__ vs prototype? prototype is a property of functions; __proto__/Object.getPrototypeOf is an object's actual link.
Q2Why put methods on the prototype?core▾

All instances share one function instead of each instance holding its own copy, saving memory. Classes do this automatically for methods.

Follow-ups interviewers ask
  • What about class fields with arrow functions? They're per-instance.
Q3How does Object.create work?core▾

Object.create(proto) creates a new object whose prototype is proto, enabling prototypal inheritance without constructors; Object.create(null) creates an object with no prototype (a clean dictionary).

Follow-ups interviewers ask
  • Why use null-prototype objects? No inherited keys like toString interfering.
Q4What is prototype pollution?senior▾

An attack where untrusted input sets properties like __proto__.isAdmin = true via unsafe merges, affecting all objects. Prevent with safe merge functions, Object.create(null), Map, schema validation, and freezing prototypes.

Follow-ups interviewers ask
  • Which operations are risky? Deep merge/clone of user-controlled JSON.
Q5How do you check own vs inherited properties?core▾

Object.hasOwn(obj, key) (or hasOwnProperty) for own properties; in checks the whole chain; Object.keys lists own enumerable properties.

Follow-ups interviewers ask
  • Why prefer Object.hasOwn? Works even when hasOwnProperty is shadowed or missing.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.