JavaScript Prototypes and the Prototype Chain
JavaScript prototypes are the reason objects can use methods they do not own. When you read a property, JavaScript checks the object first, then walks up a linked chain of prototype objects until it finds the property or reaches the end. Class syntax did not replace that model. It dressed it in cleaner clothes.
If you understand property lookup, class, extends, instanceof, and a lot of "where did this method come from?" moments become much less mysterious.
Property lookup starts on the object
Start with a plain object:
const user = {
name: "Ada",
};
console.log(user.name); // "Ada"
name is an own property. It lives directly on user.
Now call a method you did not define:
console.log(user.toString());
// "[object Object]"
user does not have its own toString property. JavaScript finds toString on Object.prototype, which is linked as the prototype of normal object literals.
That hidden link is the object's [[Prototype]].
The prototype chain
You can create a direct prototype link with Object.create:
const animal = {
breathes: true,
};
const dog = Object.create(animal);
dog.legs = 4;
console.log(dog.legs); // 4
console.log(dog.breathes); // true
Lookup works like this:
- Check
dogforlegs. - If the property is not there, check
animal. - If it is not there, keep walking upward.
- Stop at
null.
If the property is never found, the result is undefined.
console.log(dog.wings); // undefined
This is prototypal inheritance: objects linked to other objects.
Reading an object's prototype
Use Object.getPrototypeOf:
Object.getPrototypeOf(dog) === animal; // true
Object.getPrototypeOf(animal) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype); // null
You may see __proto__ in older examples. Avoid it in new code. Object.getPrototypeOf is the clearer read API. Object.setPrototypeOf exists for changing a prototype, but changing prototypes after objects are created can hurt performance and make behavior harder to reason about.
Set the prototype at creation time when you can.
Object.create
Object.create(proto) makes a new object whose prototype is proto:
const sharedMethods = {
describe() {
return `${this.name} has ${this.wheels} wheels`;
},
};
const bike = Object.create(sharedMethods);
bike.name = "Bike";
bike.wheels = 2;
console.log(bike.describe());
// "Bike has 2 wheels"
describe lives on sharedMethods. When called as bike.describe(), this is still bike, because this is determined by the call site.
You can also create an object with no prototype:
const dictionary = Object.create(null);
dictionary.course = "javascript";
console.log(dictionary.toString); // undefined
Objects with a null prototype are useful for pure dictionaries, because they do not inherit keys from Object.prototype.
Constructor functions and .prototype
Before class, constructor functions were the common object factory pattern:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Hello, ${this.name}`;
};
const ada = new User("Ada");
console.log(ada.greet()); // "Hello, Ada"
When you call new User("Ada"), JavaScript:
- Creates a new object.
- Sets that object's prototype to
User.prototype. - Calls
Userwiththisset to the new object. - Returns the new object unless the constructor returns another object.
The confusing naming is unfortunate: a function's .prototype property is not the same thing as an object's internal [[Prototype]]. The function's .prototype becomes the internal prototype of instances created with new.
Object.getPrototypeOf(ada) === User.prototype; // true
Methods on prototypes are shared
If you define a method inside the constructor, every instance gets a separate function:
function User(name) {
this.name = name;
this.greet = function () {
return `Hello, ${this.name}`;
};
}
If you define it on the prototype, instances share one function:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Hello, ${this.name}`;
};
That is the main reason prototype methods exist: shared behavior without copying the method onto every object.
Class syntax uses prototypes
This class:
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
still puts greet on User.prototype:
const ada = new User("Ada");
Object.getPrototypeOf(ada) === User.prototype; // true
class syntax is not fake, but it is not a different inheritance engine. It is a cleaner syntax over prototype-based mechanics.
With extends, the prototype chain connects subclasses to parent prototypes:
class Animal {
speak() {
return "sound";
}
}
class Dog extends Animal {
speak() {
return "bark";
}
}
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
If a method is not found on Dog.prototype, lookup continues to Animal.prototype.
Own properties shadow prototype properties
An own property wins over a prototype property with the same name:
const defaults = {
role: "viewer",
};
const user = Object.create(defaults);
console.log(user.role); // "viewer"
user.role = "admin";
console.log(user.role); // "admin"
console.log(defaults.role); // "viewer"
The prototype property was not changed. It was shadowed. Delete the own property and the prototype value appears again:
delete user.role;
console.log(user.role); // "viewer"
instanceof checks the chain
instanceof checks whether a constructor's .prototype appears anywhere in an object's prototype chain.
class Animal {}
class Dog extends Animal {}
const rover = new Dog();
console.log(rover instanceof Dog); // true
console.log(rover instanceof Animal); // true
It does not check whether the constructor literally created the object in some permanent historical record. It checks the current prototype relationship.
That means changing prototypes can make instanceof surprising:
function Widget() {}
const widget = new Widget();
Widget.prototype = {};
console.log(widget instanceof Widget); // false
The object still has its original prototype. The constructor now points at a different .prototype object.
Prototype pollution
Prototype pollution happens when untrusted input manages to modify a shared prototype. Once that happens, many unrelated objects can appear to have properties they never defined.
The vulnerable shape is often a deep merge that trusts keys:
function merge(target, source) {
for (const key in source) {
const value = source[key];
if (value && typeof value === "object") {
target[key] ??= {};
merge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);
console.log({}.isAdmin);
// true in vulnerable merge patterns
The defense is boring and necessary:
- Reject dangerous keys such as
__proto__,prototype, andconstructorwhen merging untrusted data. - Prefer
Object.create(null)for dictionary targets. - Use well-maintained merge utilities that handle prototype-pollution cases.
- Avoid writing to global prototypes in application code.
Prototype pollution is not an inheritance curiosity. It is a real security class caused by forgetting that prototypes are shared.
Key Takeaways
- Property lookup checks the object first, then walks the prototype chain.
Object.getPrototypeOf()shows the internal prototype link.Object.create(proto)creates an object with a chosen prototype.- Constructor
.prototypebecomes the internal prototype of instances created withnew. classandextendsstill use prototype mechanics.- Own properties shadow prototype properties.
- Prototype pollution happens when untrusted keys modify shared prototypes.