structural-engineering-and-design
Understanding Javascript Prototype Chain and Inheritance
Table of Contents
What Is the Prototype Chain?
JavaScript is one of the few widely used languages that implements prototypal inheritance instead of classical inheritance. At the core of this system is the prototype chain — a mechanism that allows objects to inherit properties and methods from other objects. Every JavaScript object has an internal, hidden property known as [[Prototype]] (often accessed via the deprecated __proto__ or the standard Object.getPrototypeOf() method). This property points to another object, known as the prototype. When you try to access a property on an object, JavaScript first looks at the object’s own properties. If it doesn’t find the property there, it follows the [[Prototype]] link to the next object in the chain, and so on, until either the property is found or the chain ends at null. This chain of linked prototypes is what enables inheritance in JavaScript.
For example, consider a simple object literal:
const animal = {
eats: true,
walk() {
console.log("Animal walks");
}
};
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.eats); // true (inherited)
console.log(rabbit.jumps); // true (own property)
rabbit.walk(); // "Animal walks" (inherited)
Here, rabbit is created with its [[Prototype]] set to animal. The property eats is not on rabbit itself, so JavaScript goes up the chain to animal and finds it. The walk method is also inherited. This lookup process continues until it reaches Object.prototype and finally null. Understanding this chain is essential for writing efficient and predictable code in JavaScript. For more official details, see MDN: Inheritance and the prototype chain.
The Difference Between [[Prototype]] and the prototype Property
A common source of confusion is the distinction between the internal [[Prototype]] (the actual link from an object to its prototype) and the public prototype property that exists on constructor functions. The prototype property is not the prototype of the function itself — it is the object that will become the [[Prototype]] of any instances created when that function is called with new. For example:
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
return "Woof!";
};
const fido = new Dog("Fido");
console.log(Object.getPrototypeOf(fido) === Dog.prototype); // true
The instance fido has its internal [[Prototype]] set to Dog.prototype. The function Dog itself also has a [[Prototype]] (pointing to Function.prototype), but that is separate from its prototype property.
How Inheritance Works in JavaScript
Inheritance in JavaScript is achieved entirely through this prototype mechanism. There are several ways to set up an inheritance relationship between objects: using constructor functions, the Object.create() method, and the ES6 class syntax (which is syntactic sugar over the prototype system). Each method has its use cases and trade-offs.
Constructor Functions and the Prototype Property
Before ES6, the most common way to create objects that share methods was to use a constructor function and assign methods to its prototype property. When you call the function with new, a new object is created, its [[Prototype]] is linked to the function’s prototype, and the constructor’s code runs in the context of the new object (with this bound to it).
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound.`;
};
const cat = new Animal("Whiskers");
console.log(cat.speak()); // "Whiskers makes a sound."
When cat.speak is called, JavaScript looks for speak on cat itself. Not finding it, it goes to cat's prototype, which is Animal.prototype, and finds the method there. If we wanted to create a subtype, say Cat that inherits from Animal, we would need to manually set up the prototype chain:
function Cat(name, color) {
Animal.call(this, name); // call parent constructor
this.color = color;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // fix constructor pointer
Cat.prototype.meow = function() {
return `${this.name} meows.`;
};
const kitty = new Cat("Mittens", "gray");
console.log(kitty.speak()); // "Mittens makes a sound." (inherited)
console.log(kitty.meow()); // "Mittens meows."
Here, Cat.prototype is replaced with a new object whose prototype is Animal.prototype. That means instances of Cat will inherit from both Cat.prototype and Animal.prototype. The constructor property is reset so that kitty.constructor points back to Cat — a common housekeeping step for accurate introspection.
Using Object.create() for Direct Inheritance
The Object.create() method provides a cleaner way to create an object with a specific prototype without using constructors. This is especially useful for simpler inheritance patterns or for creating objects that need to inherit from a plain object.
const vehicle = {
drive() {
return "Vroom!";
}
};
const car = Object.create(vehicle);
car.wheels = 4;
console.log(car.drive()); // "Vroom!"
console.log(Object.getPrototypeOf(car) === vehicle); // true
You can also pass a second argument to Object.create() to define additional properties with property descriptors, but for most cases the single-argument form suffices. This approach avoids the confusion of constructor functions and is ideal when you want a simple delegation chain.
ES6 Classes: Syntactic Sugar Over Prototypes
ES6 introduced the class syntax, which provides a cleaner, more familiar way to write constructors and handle inheritance. Under the hood, classes still use the prototype chain; the syntax simply hides the complexity.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Cat extends Animal {
constructor(name, color) {
super(name); // call parent constructor
this.color = color;
}
meow() {
return `${this.name} meows.`;
}
}
const kitty = new Cat("Mittens", "gray");
console.log(kitty.speak()); // "Mittens makes a sound."
console.log(kitty.meow()); // "Mittens meows."
Notice the extends keyword and the super() call. extends sets up the prototype chain automatically: Cat.prototype’s prototype becomes Animal.prototype, and the [[Prototype]] of the Cat class (the constructor) is set to Animal (allowing static method inheritance). This syntax is preferred in modern codebases for its readability and ease of maintenance. For an in-depth comparison of the two approaches, see this analysis of ES6 classes.
Understanding the Prototype Chain in Practice
Knowing how the prototype chain works is not just an academic exercise — it directly affects everyday coding decisions. Let’s examine some practical aspects, including property lookup, shadowing, and common methods for inspecting prototypes.
Property Lookup and Shadowing
When you assign a property directly to an object, it is stored as an own property. If there is already a property with the same name somewhere up the prototype chain, the own property shadows the inherited one. For example:
const parent = { value: 42 };
const child = Object.create(parent);
console.log(child.value); // 42 (inherited)
child.value = 99;
console.log(child.value); // 99 (own property shadows)
console.log(parent.value); // 42 (unchanged)
delete child.value;
console.log(child.value); // 42 (inherited again)
This behavior is deterministic: JavaScript always checks own properties first. If you need to check whether a property is own or inherited, use hasOwnProperty() (or Object.hasOwn() in modern engines).
Inspecting the Prototype Chain
Several built-in methods help you examine an object’s prototype chain:
Object.getPrototypeOf(obj)— returns the immediate prototype ofobj.obj.isPrototypeOf(otherObj)— checks ifobjappears anywhere in the prototype chain ofotherObj.obj instanceof Constructor— returnstrueif the prototype chain ofobjcontainsConstructor.prototype.Object.hasOwn(obj, prop)— returnstrueifpropis an own property ofobj(recommended overhasOwnProperty).
Example using instanceof:
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
const myDog = new Dog();
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
console.log(myDog instanceof Object); // true
Performance Considerations
Property lookups that travel up a long prototype chain incur a performance cost, though modern JavaScript engines optimize heavily for common patterns. Creating extremely deep chains (e.g., inheritance hierarchies with 10+ levels) can slow down property access. Prefer shallow, composable structures when possible. Also, be aware that modifying an object’s prototype after creation (via Object.setPrototypeOf() or obj.__proto__) can cause performance degradation because engines must deoptimize the object’s shape. Avoid changing prototypes of objects that will be accessed frequently. For more on performance, refer to V8’s blog on fast property access.
Common Pitfalls and Best Practices
Even experienced developers can stumble when working with prototypes. Here are some pitfalls to avoid and best practices to adopt.
Avoid Modifying Built-in Prototypes
While it is technically possible to add methods to Object.prototype or Array.prototype, doing so is strongly discouraged. Any custom property added to a built-in prototype will appear in for...in loops and can break code that expects the object to contain only standard iterables. If you need to add helper methods, consider using a separate utility function or a polyfill that checks for the existence of the method. For example, never do Array.prototype.myMethod = ... unless you are polyfilling a standard method.
Use Object.create(null) for Pure Dictionaries
Objects created with Object.create(null) have no prototype chain (their [[Prototype]] is null). This eliminates the risk of accidental property name collisions with properties inherited from Object.prototype (like toString, hasOwnProperty). Use this pattern for dictionaries or maps where you control all keys:
const dict = Object.create(null);
dict["key"] = "value";
console.log(dict.toString); // undefined (no inheritance)
console.log("toString" in dict); // false
Handle Constructor Property Correctly
When you manually set the prototype of a constructor (e.g., Child.prototype = Object.create(Parent.prototype)), the constructor property on Child.prototype now points to Parent (or is missing if you didn’t set it). This can break code that relies on instance.constructor to identify the object’s type. Always restore the constructor property:
Child.prototype.constructor = Child;
Or better, use ES6 classes which handle this automatically.
Prefer Composition Over Inheritance
While prototypes enable inheritance, deep inheritance trees can become brittle. Many experienced JavaScript developers recommend favoring composition over inheritance: instead of having a class that extends another, combine multiple objects or use mixins. For example, instead of class FlyingCat extends Cat, create a canFly object and compose it into a new object using Object.assign or mixin functions. This keeps the prototype chain shallow and makes code easier to refactor. For a detailed discussion, see Eric Elliott’s article on composition vs inheritance.
Summary and Next Steps
The prototype chain is the backbone of JavaScript’s inheritance model. It allows objects to share behavior efficiently through a simple delegation mechanism. By understanding how [[Prototype]] links work, how to set up inheritance via constructors, Object.create(), or classes, and how to inspect the chain, you can write more robust and maintainable code. Remember to avoid common pitfalls like modifying built-in prototypes or creating overly deep chains, and consider composition when the inheritance hierarchy becomes complex.
To deepen your knowledge, explore the You Don’t Know JS series by Kyle Simpson, which provides an exhaustive explanation of prototypes and the this keyword. Practice by building your own inheritance scenarios in the browser console or Node.js, and experiment with the methods discussed here. Mastering the prototype chain will give you a solid foundation for advanced JavaScript patterns and frameworks.