Prototypes and Prototype Chain in JavaScript
Understand how JavaScript's prototype chain works, prototype-based inheritance, and how ES6 classes relate to prototypes under the hood.
JavaScript is a prototype-based language. Every object has an internal link to another object called its prototype. Property lookups walk this chain until the value is found or the chain ends at null. Understanding this mechanism is essential because it underpins everything from method inheritance to the class syntax introduced in ES6.
The Prototype Chain
When you access a property on an object, JavaScript first looks at the object itself. If not found, it walks up the prototype chain — checking each linked object in sequence. This delegation model means objects can share behavior without copying it. Every method you call on an array, for example, lives on Array.prototype, not on the array itself.
const animal = {
breathe() {
return `${this.name} is breathing`;
},
};
const dog = Object.create(animal); // dog's prototype is animal
dog.name = "Rex";
console.log(dog.breathe()); // "Rex is breathing" — found on animal via chain
console.log(dog.hasOwnProperty("name")); // true — own property
console.log(dog.hasOwnProperty("breathe")); // false — inherited from animal
console.log(Object.getPrototypeOf(dog) === animal); // true
The chain here is: dog → animal → Object.prototype → null.
__proto__ vs prototype
These two properties are often confused because they sound similar but serve different purposes. prototype is a property on constructor functions — it is the object that gets assigned as __proto__ on any instance created with new. __proto__ is the actual prototype link on an object instance, and it is what gets walked during property lookup.
function Person(name) {
this.name = name;
}
// `Person.prototype` is the object that becomes __proto__ of instances
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const alice = new Person("Alice");
// On instances, use Object.getPrototypeOf() — __proto__ is deprecated
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice.__proto__ === Person.prototype); // true (avoid in prod)
console.log(alice.greet()); // "Hi, I'm Alice"
Rule of thumb: use Object.getPrototypeOf() to read a prototype and Object.create() to set one. Avoid writing to __proto__ directly.
Object.create for Clean Inheritance
Object.create(proto) creates a new object whose prototype is proto. This is the most explicit and readable way to establish a prototype relationship — you can see exactly what you’re inheriting from without the noise of new and constructor wiring.
const vehicleProto = {
describe() {
return `${this.make} ${this.model} (${this.year})`;
},
start() {
return `${this.make} engine started`;
},
};
function createCar(make, model, year) {
// Create a new object that delegates to vehicleProto
const car = Object.create(vehicleProto);
car.make = make;
car.model = model;
car.year = year;
return car;
}
const tesla = createCar("Tesla", "Model 3", 2024);
console.log(tesla.describe()); // "Tesla Model 3 (2024)"
console.log(tesla.start()); // "Tesla engine started"
Prototype-Based Inheritance with Constructor Functions
Before ES6 classes, this constructor function pattern was the standard way to implement inheritance in JavaScript. It is still important to understand because it reveals what the class syntax is doing under the hood, and because you will encounter it in older codebases.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // call parent constructor to initialize own properties
this.breed = breed;
}
// Wire up the prototype chain — Dog instances delegate to Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // restore constructor reference (gets overwritten above)
Dog.prototype.bark = function () {
return `${this.name} barks!`;
};
const rex = new Dog("Rex", "Labrador");
console.log(rex.speak()); // "Rex makes a sound" — inherited from Animal.prototype
console.log(rex.bark()); // "Rex barks!" — own method on Dog.prototype
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
How ES6 Classes Relate to Prototypes
ES6 class syntax is syntactic sugar over the constructor function pattern above. The JavaScript engine compiles it to the same prototype-based mechanism — there are no new runtime semantics. Classes are cleaner to write and read, but understanding prototypes means you can debug class-based code at the prototype level when needed.
class Animal {
constructor(name) {
this.name = name;
}
// speak lives on Animal.prototype — shared by all instances
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // calls Animal's constructor
this.breed = breed;
}
bark() {
return `${this.name} barks!`;
}
}
const rex = new Dog("Rex", "Labrador");
// Verify the prototype chain is identical to the manual approach above
console.log(typeof Dog); // "function" — classes are constructor functions
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Dog.prototype.hasOwnProperty("bark")); // true — bark is on the prototype
Prototype Methods vs Instance Methods
Methods defined inside the constructor body are re-created as new function objects for every instance. Methods defined on the prototype (or in the class body, which goes to the prototype) are created once and shared. For classes with many instances, keeping methods on the prototype significantly reduces memory usage.
class Counter {
constructor(start = 0) {
this.count = start;
// BAD: a new function object is allocated for every Counter instance
this.reset = function () {
this.count = 0;
};
}
// GOOD: increment and value are defined once on Counter.prototype
increment() {
this.count++;
}
value() {
return this.count;
}
}
const a = new Counter();
const b = new Counter();
// Prototype methods share a single function reference
console.log(a.increment === b.increment); // true — same function object
// Instance methods are distinct per instance
console.log(a.reset === b.reset); // false — two separate function objects
For a class with thousands of instances, putting methods on the prototype instead of the constructor measurably reduces memory usage.
Prototype Pollution — A Real Danger
Prototype pollution occurs when untrusted input is merged into Object.prototype. Because every plain object inherits from Object.prototype, adding a property there affects all objects in the application — a subtle but critical security vulnerability that has affected many popular libraries.
// Vulnerable pattern — never merge untrusted data without guards
function merge(target, source) {
for (const key in source) {
target[key] = source[key]; // no hasOwnProperty guard!
}
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
console.log({}.isAdmin); // true — every object in the app is now "admin"!
Safe alternatives:
// Option 1: guard with hasOwnProperty — only copy own properties, not inherited ones
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
// Option 2: use Object.create(null) for lookup tables — no prototype chain to pollute
const safe = Object.create(null); // safe has no __proto__, no toString, no hasOwnProperty
safe.name = "ok";
Key Takeaways
- Every object has a prototype; lookups walk the chain until
null. Object.create(proto)is the cleanest way to establish inheritance.- Use
Object.getPrototypeOf()instead of__proto__. - ES6 classes compile to the same prototype mechanism — no magic.
- Never mutate
Object.prototype; always guard against prototype pollution.