Prototypal Inheritance
How JS objects delegate via [[Prototype]] — chains, Object.create, constructors, classes as sugar, own vs inherited props, and common footguns.
- javascript
- prototype
- inheritance
- objects
JavaScript’s core object model is delegation, not classical copy-the-template inheritance. Every ordinary object has an internal link [[Prototype]] (exposed as __proto__ or via Object.getPrototypeOf). When you read a property the object does not own, the engine walks the chain until it finds the property or hits null.
That link is prototypal inheritance. Classes, constructors, and extends are syntax and conventions on top of the same chain.
If you only memorize “__proto__ vs prototype,” you’ll still fail the interview follow-up: where does a method lookup go, and what does assignment write?
The problem this model solves
You want many objects to share behavior without cloning function bodies onto each instance:
const animal = {
eat() {
return `${this.name} eats`;
},
};
const dog = Object.create(animal);
dog.name = 'Rex';
dog.eat(); // "Rex eats" — method found on animal, this === dog
One eat function, many animals. Memory stays flat; behavior stays consistent.
Model: own properties first, then the chain
const proto = { kind: 'proto', shared: true };
const obj = Object.create(proto);
obj.kind = 'own';
console.log(obj.kind); // "own" — own property wins
console.log(obj.shared); // true — inherited
console.log(obj.missing); // undefined — chain ended
| Operation | Behavior |
|---|---|
Get obj.x |
Own → [[Prototype]] → … → null |
Set obj.x = v |
Usually creates/updates own property (does not edit the prototype’s x unless it’s an accessor with a setter) |
in operator |
True if found anywhere on the chain |
hasOwnProperty / Object.hasOwn |
True only for own properties |
const p = { a: 1 };
const c = Object.create(p);
console.log('a' in c); // true
console.log(Object.hasOwn(c, 'a')); // false
c.a = 2; // shadows — own `a` now
console.log(p.a); // 1 — prototype unchanged
Shadowing is how instances customize shared defaults without mutating the prototype for everyone.
__proto__ vs .prototype (say this cleanly)
Two different things people overload with the word “prototype”:
obj.[[Prototype]]— the object used for lookup onobj. Read withObject.getPrototypeOf(obj).Fn.prototype— the object that will become[[Prototype]]of instances created withnew Fn().
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi, ${this.name}`;
};
const p = new Person('Ada');
Object.getPrototypeOf(p) === Person.prototype; // true
p.greet(); // "Hi, Ada"
p.hasOwnProperty('greet'); // false — method lives on Person.prototype
new roughly:
- Create a new object.
- Set its
[[Prototype]]toConstructor.prototype. - Call
Constructorwiththisbound to that object. - Return the object (unless the constructor returns another object).
More on construction and this: this Binding Rules.
Object.create and null objects
const dict = Object.create(null);
dict.answer = 42;
// dict.toString — undefined; no Object.prototype
console.log('toString' in dict); // false
Null-prototype objects are excellent as pure maps (no inherited keys, no constructor surprises). Prefer Map when key types or iteration ergonomics matter; use Object.create(null) when you need a plain JSON-like bag without prototype pollution footguns.
// Dangerous if keys come from users and you inherit Object.prototype
const bag = {};
// bag['__proto__'] = ... historical issues; also hasOwn checks matter
Constructor chains before class
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a noise`;
};
function Dog(name, breed) {
Animal.call(this, name); // own props from parent
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
return `${this.name} barks`;
};
const d = new Dog('Rex', 'mutt');
d.speak(); // "Rex barks"
d instanceof Dog; // true
d instanceof Animal; // true
Pattern pieces:
Parent.call(this, …)initializes own state.Child.prototype = Object.create(Parent.prototype)wires delegation.- Reset
constructorif you care aboutinstance.constructor. - Put methods on
.prototype, not onthisinside the constructor (unless per-instance state requires it).
call here is explicit binding — call, apply, and bind.
class is mostly sugar (know the desugaring)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
}
Under the hood you still get prototype links. Differences that matter:
- Class bodies run in strict mode.
- Methods are non-enumerable.
superhas precise lookup rules.- Classes are not hoisted like function declarations — they sit in the TDZ.
- Calling a class without
newthrows.
typeof Dog; // "function"
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
Object.getPrototypeOf(Dog) === Animal; // static side chain
Static methods live on the constructor function, not on instances.
Property lookup vs assignment (the classic trap)
function Counter() {}
Counter.prototype.count = 0;
const a = new Counter();
const b = new Counter();
a.count += 1; // read 0 from proto, write own a.count = 1
console.log(a.count); // 1
console.log(b.count); // 0 — still on prototype
console.log(Counter.prototype.count); // 0
Mutating object values on a shared prototype is worse:
function Team() {}
Team.prototype.members = []; // shared mutable — usually a bug
const t1 = new Team();
const t2 = new Team();
t1.members.push('A');
console.log(t2.members); // ['A'] — surprise coupling
Initialize mutable state in the constructor (this.members = []), share only immutable defaults or methods on the prototype.
instanceof and isPrototypeOf
d instanceof Dog; // walks d's chain looking for Dog.prototype
Dog.prototype.isPrototypeOf(d); // true
instanceof can be faked with Symbol.hasInstance. For pure chain checks, isPrototypeOf is direct. Across realms (iframes), instanceof Array can fail — prefer Array.isArray.
Modern alternatives you should name
| Need | Prefer |
|---|---|
| Share behavior | Prototype methods / class |
| Compose behavior | Mixins carefully, or plain functions + closures |
| Dictionary | Map or Object.create(null) |
| Privacy | Closures or #private fields |
| Multiple “parents” | Composition over deep prototype trees |
Prototypes are not bad. Deep fragile hierarchies are. Frontend codebases usually win with shallow prototypes (one class or factory) and composition for cross-cutting behavior.
Footguns
- Confusing
Fn.prototypewithinstance.__proto__— related, not identical names. - Mutable state on
.prototype. - Forgetting
Parent.call(this)when hand-rolling inheritance — child instances miss parent own fields. - Arrow methods on prototypes via class fields —
class C { m = () => this }puts a per-instance arrow (often intentional forthis, costly if overused). - Iterating with
for…inwithouthasOwnfilters — picks up enumerable inherited keys. - Assuming JSON or structured clones keep prototype methods — they don’t; you get data shapes, not class instances.
Interview angle
Prompt: “Explain prototypal inheritance.”
Strong answer: “Objects have an internal [[Prototype]] link. Property reads walk that chain; writes usually create own properties. Constructors and classes set up instance → Constructor.prototype → … → Object.prototype → null. Methods live on the shared prototype so instances share one function. Object.create(proto) builds delegation without a constructor. class/extends desugar to the same links with stricter syntax.”
Whiteboard: Draw the chain for class Dog extends Animal, then predict hasOwn for a method name vs an own field.
Follow-ups: difference between __proto__ and prototype; why instanceof fails across frames; how closures give privacy without prototypes.
Related on this site
- Object.create and the Prototype Chain — API-focused companion
- call, apply, and bind — wiring parent constructors
- this Binding Rules — how methods get their receiver
- Closures — alternative to prototype privacy
- Classes in JavaScript — syntax layer
Further reading
- Inheritance and the prototype chain — MDN
- Object.create — MDN
- Object.getPrototypeOf — MDN
- javascript.info — Prototypal inheritance
Related guides
- Classes in JavaScriptJS classes as sugar over prototypes — constructors, extends, super, fields, and what still differs from classical OOP.
- Destructuring Objects and ArraysObject and array destructuring — renames, defaults, nested patterns, rest, and parameter destructuring in real APIs.
- Getters and Settersget/set accessors on objects and classes — computed properties, validation, infinite loop traps, and defineProperty.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.