What Is the Prototype Chain in JavaScript and How Does Inheritance Work?
The prototype chain in JavaScript is a delegation mechanism where every object has an internal [[Prototype]] reference (accessible via Object.getPrototypeOf) that links to another object, forming a chain that the engine traverses upward to resolve property lookups until reaching Object.prototype whose prototype is null.
The leonardomso/33-js-concepts repository demonstrates these mechanics through comprehensive test suites. Understanding the prototype chain in JavaScript is fundamental to working with object-oriented patterns, as it governs how properties are inherited, how instanceof operates, and how memory is shared across instances.
Understanding the Core Prototype Chain Mechanism
Every JavaScript object contains an internal slot called [[Prototype]], exposed in code through Object.getPrototypeOf(obj) or the legacy __proto__ accessor. When you access a property or method, the engine first checks the object itself; if the property is missing, it follows the [[Prototype]] link to the next object and repeats the search. This continues recursively until the property is found or the chain terminates at Object.prototype (whose own prototype is null).
This linked structure is the prototype chain. It enables delegation-based inheritance rather than class-based copying, meaning objects inherit references to shared methods rather than holding separate copies.
Class-Based Inheritance and the Double Link
When you declare class Child extends Parent, JavaScript sets up two distinct prototype relationships:
- Instance delegation:
new Child()produces an object whose[[Prototype]]points toChild.prototype - Prototype delegation:
Child.prototype's[[Prototype]]points toParent.prototype
As verified in tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js (lines 3-10), this creates a three-tier chain for any Warrior instance extending Character:
Object.getPrototypeOf(warrior) === Warrior.prototype
Object.getPrototypeOf(Warrior.prototype) === Character.prototype
Object.getPrototypeOf(Character.prototype) === Object.prototype
Method Resolution Order
Property lookup follows this chain sequentially. The test suite (lines 15-21) confirms that Warrior.prototype.hasOwnProperty('attack') returns true because attack is defined directly on the child prototype, while Character.prototype.hasOwnProperty('introduce') returns true for inherited methods. If warrior.introduce() is called, the engine finds it on Character.prototype after failing to locate it on Warrior.prototype or the warrior instance itself.
The super Keyword and Static Inheritance
The super keyword provides explicit access to the parent class within subclass definitions. Inside constructors, super(args) invokes the parent constructor. Inside methods, super.method() climbs the prototype chain to execute the parent implementation.
The repository's ExtendedWarrior example (lines 50-60) demonstrates this pattern:
class ExtendedWarrior extends Character {
attack() {
const base = super.attack() // Calls Character.prototype.attack
return `${base} With an Axe!`
}
}
Static Member Inheritance
Static methods and properties live on the constructor functions themselves, not on .prototype. When class Warrior extends Character is declared, JavaScript sets Object.getPrototypeOf(Warrior) === Character, enabling static inheritance. The test suite (lines 31-35) validates that Warrior.createRandom() works because it inherits the static createRandom method from Character via the constructor's prototype chain.
Checking Prototype Relationships with instanceof
The instanceof operator checks the entire prototype chain, not just the immediate constructor. As shown in the test file (lines 68-74), a Warrior instance returns true for warrior instanceof Warrior, warrior instanceof Character, and warrior instanceof Object because all three prototypes exist in the object's chain:
console.log(warrior instanceof Warrior) // true
console.log(warrior instanceof Character) // true
console.log(warrior instanceof Object) // true
This works by verifying whether Warrior.prototype (or any object in its prototype chain) exists in warrior's [[Prototype]] chain.
Practical Implementation Example
The following runnable code extracted from the repository demonstrates the complete inheritance pattern, including constructor delegation, method overriding, and polymorphic behavior:
// Base class definition
class Character {
constructor(name, health = 100) {
this.name = name
this.health = health
}
introduce() {
return `I am ${this.name} with ${this.health} HP`
}
attack() {
return `${this.name} attacks!`
}
static createRandom() {
const names = ['Hero', 'Villain', 'Sidekick']
return new this(names[Math.floor(Math.random() * names.length)])
}
}
// Subclass declaration
class Warrior extends Character {
constructor(name) {
super(name, 150) // Delegates to Character constructor
this.rage = 0
}
attack() {
// Overrides parent method
return `${this.name} swings a mighty sword!`
}
battleCry() {
this.rage += 10
return `${this.name} roars! Rage: ${this.rage}`
}
}
// Prototype chain verification
const conan = new Warrior('Conan')
console.log(Object.getPrototypeOf(conan) === Warrior.prototype) // true
console.log(Object.getPrototypeOf(Warrior.prototype) === Character.prototype) // true
// Property lookup demonstration
console.log(conan.attack()) // Uses Warrior.prototype.attack
console.log(conan.introduce()) // Uses Character.prototype.introduce (inherited)
// Static inheritance
const randomWarrior = Warrior.createRandom()
console.log(randomWarrior instanceof Warrior) // true
console.log(randomWarrior instanceof Character) // true
// Polymorphic function operating on shared interface
function executeBattle(chars) {
return chars.map(c => c.attack())
}
console.log(executeBattle([new Warrior('A'), new Character('B')]))
// Output: ["A swings a mighty sword!", "B attacks!"]
Summary
- Every JavaScript object links to another object via its internal
[[Prototype]]slot, forming the prototype chain. - Class inheritance using
extendscreates two links: one from instance to child prototype, and one from child prototype to parent prototype. - The
superkeyword explicitly accesses parent class constructors and methods, while static members inherit through the constructor's own prototype chain. instanceoftraverses the entire prototype chain to determine membership, returningtruefor any ancestor constructor in the hierarchy.- This delegation model enables memory-efficient method sharing and polymorphic design patterns, as demonstrated in
tests/object-oriented/inheritance-polymorphism/inheritance-polymorphism.test.js.
Frequently Asked Questions
What is the difference between __proto__ and prototype?
__proto__ is an accessor property that exposes the internal [[Prototype]] of an instance object, while prototype is a regular property on constructor functions that becomes the [[Prototype]] of instances created with that constructor. In modern JavaScript, use Object.getPrototypeOf() and Object.setPrototypeOf() instead of __proto__ for better performance and standards compliance.
How does the instanceof operator actually work?
instanceof checks if the constructor's prototype property exists anywhere in the object's prototype chain. Specifically, obj instanceof Constructor evaluates whether Constructor.prototype is reachable from obj following the [[Prototype]] links upward until null. This is why a Warrior instance returns true for both Warrior and Character checks.
Can you modify an object's prototype after it is created?
Yes, though it is generally discouraged for performance reasons. You can change an object's prototype using Object.setPrototypeOf(obj, newProto) or by setting __proto__, but doing so invalidates engine optimizations. It is better to set the prototype at creation time using Object.create(newProto) or class extends declarations.
Why does static inheritance work differently than instance inheritance?
Static members belong to the constructor function itself, not the prototype object. When class B extends A is parsed, JavaScript internally performs Object.setPrototypeOf(B, A), linking the constructor functions in a chain parallel to the instance prototype chain. This allows B to access static methods defined on A, such as B.createRandom() delegating to A.createRandom().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →