Prototypal Inheritance
How JavaScript objects delegate behavior through the prototype chain β from __proto__ links to ES6 classes.
JavaScript doesn't have classical inheritance. Instead, objects are linked to other objects via a prototype chain. When you access a property, the engine walks this chain until it finds the property or reaches null. Classes, constructors, and 'extends' are all syntactic sugar over this mechanism.
Unlike classical inheritance (classes creating copies), JavaScript uses delegation. Objects are linked to other objects via a [[Prototype]] reference. When a property isn't found on an object, the engine follows this link to search the next object in the chain.
When you access obj.prop: 1) Check if 'prop' exists on obj itself (own property). 2) If not, follow [[Prototype]] to the next object. 3) Repeat until found or null is reached. 4) If null, return undefined (or TypeError for method calls).
When you call 'new Foo()': 1) A new empty object is created. 2) Its [[Prototype]] is set to Foo.prototype. 3) 'this' inside Foo refers to the new object. 4) The object is returned. This is how methods on .prototype are shared across all instances.
ES6 'class' and 'extends' keywords compile down to prototype chains. 'class Dog extends Animal' just means Dog.prototype.__proto__ === Animal.prototype. Understanding this reveals why 'super' works and what 'static' methods really are.
Key Concepts
Hidden internal link from every object to its prototype. Accessed via Object.getPrototypeOf() or the deprecated __proto__ property.
A regular property on constructor FUNCTIONS (not instances). When you call 'new Foo()', the new object's [[Prototype]] is set to Foo.prototype.
Own properties exist directly on the object. Inherited properties are found by walking the prototype chain. obj.hasOwnProperty('x') distinguishes them.
If an object and its prototype both define 'name', the object's own property 'shadows' the prototype's. The chain stops at the first match.
Creates a new object with the specified prototype. Object.create(null) creates a truly empty object with NO prototype β useful for dictionaries.
1// Every object has a hidden [[Prototype]] link:23const animal = {4 speak() { return `${this.name} speaks`; }5};67// Object.create sets the prototype chain:8const dog = Object.create(animal);9dog.name = "Rex";1011dog.speak(); // "Rex speaks" β found on animal12dog.hasOwnProperty("name"); // true β found on Object.prototype1314// The lookup chain:15// dog β animal β Object.prototype β null1617// Checking the chain:18Object.getPrototypeOf(dog) === animal; // true19animal.isPrototypeOf(dog); // true20dog.__proto__ === animal; // true (deprecated)
Prototypal inheritance is the backbone of JavaScript's object system. Understanding it explains how method sharing works, why 'this' behaves differently in arrow vs regular functions, how class inheritance actually works, and why modifying built-in prototypes is dangerous.
Common Pitfalls
1Efficient Method Sharing via Prototype
Your app creates 10,000 User instances, each with 5 methods (login, logout, updateProfile, etc). Memory usage spikes because each instance gets its own copy of every method.
Defining methods inside the constructor (this.login = function(){...}) creates a NEW function object per instance. 10,000 users Γ 5 methods = 50,000 function objects in memory.
Move methods to User.prototype (or use class syntax, which does this automatically). All 10,000 instances share the SAME function objects via the prototype chain. Memory usage drops by ~5x.
Takeaway: The prototype chain is JavaScript's memory-efficient method sharing mechanism. This is exactly how built-in methods like Array.prototype.map work β one copy shared by every array instance.
2Monkey-Patching Gone Wrong
A library extends Array.prototype with a custom .flatten() method. Later, ES2019 adds Array.prototype.flat(). Your code breaks because the library's method shadows the native one.
Modifying built-in prototypes (Array.prototype, Object.prototype) affects EVERY instance. If your polyfill or extension conflicts with a future native method, all arrays in the entire application are affected.
Never modify prototypes you don't own. Use utility functions (_.flatten), subclass via extends, or use Symbol-keyed methods to avoid name collisions. Check for existing properties before polyfilling.
Takeaway: The prototype chain is powerful but shared globally. Modifying shared prototypes is like editing a global variable β it affects all code that uses those objects. This is why the 'don't modify objects you don't own' rule exists.