Prototypes in JavaScript — The Complete Deep Dive
Prototypes in JavaScript — The Complete Deep Dive
Hey everyone! Welcome back!
Today we tackle one of the most misunderstood topics in JavaScript: Prototypes. If you've ever wondered how arr.map() works even though YOU never wrote a map function, or what __proto__ actually is, or how class works under the hood — the answer to all of it is prototypes.
JavaScript is a prototype-based language. There are no "real" classes under the hood — even the class keyword is just sugar over prototypes. Master this and JavaScript's object model finally makes sense. Let's go deep!
What we will cover:
- What a prototype actually is
- The prototype chain — how property lookup works
__proto__vsprototype— the confusing pair- How
newwires up prototypes - Constructor functions & adding methods to the prototype
- Prototypal inheritance
Object.create()— pure prototypal inheritance- How
classis just prototype sugar - Built-in prototypes (Array, Object, String)
- Prototype pollution & best practices
- Interview Questions
1. What is a Prototype?
Every JavaScript object has a hidden internal link to another object called its prototype. When you try to access a property on an object and it's not there, JavaScript automatically looks for it on the prototype. That's the whole idea.
┌─────────────────────────────────────────────────────────────┐ │ WHAT IS A PROTOTYPE? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Every object has a hidden link → its PROTOTYPE. │ │ │ │ Property not found on the object? │ │ → JavaScript looks on its prototype │ │ → then that prototype's prototype │ │ → ...until found, or it hits null. │ │ │ │ This is how objects SHARE behaviour without copying it. │ │ │ └─────────────────────────────────────────────────────────────┘
const person = { name: "Aditya" };
// We never defined toString on person, yet this works:
console.log(person.toString()); // "[object Object]"
// WHY? person doesn't have toString...
// but its prototype (Object.prototype) DOES.
The object person has no toString of its own — JavaScript walked up to Object.prototype, found it there, and used it. That walk is the prototype chain.
2. The Prototype Chain — How Lookup Works
When you read obj.prop, JavaScript follows these steps:
┌─────────────────────────────────────────────────────────────┐ │ PROPERTY LOOKUP ALGORITHM │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. Does the object ITSELF have 'prop'? │ │ YES → use it. STOP. │ │ NO → go to step 2. │ │ │ │ 2. Does its PROTOTYPE have 'prop'? │ │ YES → use it. STOP. │ │ NO → go to that prototype's prototype... │ │ │ │ 3. Keep climbing until found OR you reach null. │ │ Reached null? → return undefined. │ │ │ └─────────────────────────────────────────────────────────────┘
Let's visualize the chain for a normal array:
const arr = [1, 2, 3];
arr ──▶ Array.prototype ──▶ Object.prototype ──▶ null
│ │ │
│ │ └─ toString, hasOwnProperty...
│ └─ map, filter, push, forEach, ...
└─ 0, 1, 2, length
arr.map(...) → not on arr → found on Array.prototype ✔
arr.toString() → not on arr → not on Array.prototype
→ found on Object.prototype ✔
This is why every array "has" map, filter, push — they live once on Array.prototype and every array shares them through the chain. Efficient: the methods aren't copied onto each array.
3. __proto__ vs prototype — The Confusing Pair
This trips up EVERYONE. They sound the same but are different things:
┌─────────────────────────────────────────────────────────────┐ │ __proto__ vs prototype │ ├─────────────────────────────────────────────────────────────┤ │ │ │ __proto__ │ │ • Exists on EVERY object (an instance) │ │ • It's the ACTUAL link to that object's prototype │ │ • "Who is my prototype?" │ │ │ │ prototype │ │ • Exists ONLY on FUNCTIONS (constructor functions) │ │ • It's the object that will BECOME __proto__ of │ │ instances created with `new` │ │ • "What will I give to objects made from me?" │ │ │ └─────────────────────────────────────────────────────────────┘
function Dog(name) { this.name = name; }
const d = new Dog("Rex");
// prototype lives on the FUNCTION:
Dog.prototype // { constructor: Dog }
// __proto__ lives on the INSTANCE and points to Dog.prototype:
d.__proto__ // === Dog.prototype → true!
console.log(d.__proto__ === Dog.prototype); // true ✔
THE RELATIONSHIP:
Dog (function) d (instance)
┌──────────────┐ ┌──────────────┐
│ prototype ───┼──────┐ │ name: "Rex" │
└──────────────┘ │ │ __proto__ ───┼──┐
│ └──────────────┘ │
│ │
└──────────◀───────────────┘
d.__proto__ === Dog.prototype
Note: __proto__ is the old, informal accessor. The modern, official way to read/set a prototype is Object.getPrototypeOf(obj) and Object.setPrototypeOf(obj, proto).
4. How new Wires Up Prototypes
The new keyword does 4 things behind the scenes. Understanding this demystifies the whole system:
┌─────────────────────────────────────────────────────────────┐
│ WHAT `new Dog("Rex")` ACTUALLY DOES │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Create a fresh empty object: {} │
│ │
│ 2. Link its __proto__ to the function's prototype: │
│ newObj.__proto__ = Dog.prototype │
│ │
│ 3. Run the constructor with `this` = newObj: │
│ Dog.call(newObj, "Rex") → sets this.name │
│ │
│ 4. Return newObj (unless the constructor returns │
│ its own object). │
│ │
└─────────────────────────────────────────────────────────────┘
// You can even write `new` yourself to prove it:
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // steps 1 & 2
const result = Constructor.apply(obj, args); // step 3
return (typeof result === "object" && result !== null)
? result : obj; // step 4
}
const d = myNew(Dog, "Rex"); // works exactly like `new Dog("Rex")`
5. Constructor Functions & Adding Methods to the Prototype
Here's a crucial performance lesson. Compare two ways of giving dogs a bark method:
// ✗ BAD — method defined INSIDE the constructor
function Dog(name) {
this.name = name;
this.bark = function () { // a NEW function per instance!
return this.name + " says woof";
};
}
// 1000 dogs → 1000 copies of the same function → wasteful memory
// ✔ GOOD — method on the PROTOTYPE
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () { // ONE shared function
return this.name + " says woof";
};
// 1000 dogs → still ONE bark function, shared via the chain
WHY THE PROTOTYPE WAY WINS: Instance data (unique per dog) → put on `this` Shared behaviour (same for all) → put on the prototype dog1 ─┐ dog2 ─┼──▶ Dog.prototype.bark (one function for everyone) dog3 ─┘
The rule: data goes on the instance, methods go on the prototype. This is exactly what the class syntax does automatically.
6. Prototypal Inheritance
Inheritance in JavaScript = chaining prototypes together. A Dog can inherit from an Animal by making Dog.prototype's prototype be Animal.prototype.
function Animal(name) { this.name = name; }
Animal.prototype.eat = function () { return this.name + " is eating"; };
function Dog(name, breed) {
Animal.call(this, name); // 1. run Animal's constructor on `this`
this.breed = breed;
}
// 2. link Dog.prototype's prototype → Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 3. fix the constructor pointer
Dog.prototype.bark = function () { return this.name + " barks"; };
const d = new Dog("Rex", "Lab");
d.bark(); // own prototype → "Rex barks"
d.eat(); // inherited! → "Rex is eating"
THE CHAIN NOW LOOKS LIKE: d ──▶ Dog.prototype ──▶ Animal.prototype ──▶ Object.prototype ──▶ null │ │ │ │ └─ bark └─ eat └─ name, breed d.eat() → not on d → not on Dog.prototype → found on Animal.prototype ✔
7. Object.create() — Pure Prototypal Inheritance
Object.create(proto) creates a new object with its __proto__ set directly to proto — no constructor function needed. It's the purest form of prototypal inheritance.
const animal = {
eat() { return this.name + " is eating"; }
};
// Create an object whose prototype IS `animal`:
const dog = Object.create(animal);
dog.name = "Rex";
dog.eat(); // "Rex is eating" (inherited)
Object.getPrototypeOf(dog) === animal; // true ✔
Object.create(proto):
dog ──▶ animal ──▶ Object.prototype ──▶ null
Handy trick: Object.create(null) makes an object with
NO prototype at all — a "pure dictionary" with no inherited
keys like toString. Great for safe key/value maps.
8. How class is Just Prototype Sugar
ES6 class looks like classical OOP, but under the hood it's exactly the prototype machinery we just built — just cleaner syntax.
// MODERN class syntax:
class Animal {
constructor(name) { this.name = name; }
eat() { return this.name + " is eating"; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // = Animal.call(this, name)
this.breed = breed;
}
bark() { return this.name + " barks"; }
}
IT COMPILES DOWN TO: ──────────────────── • class methods → go on the .prototype (shared) • extends → Dog.prototype.__proto__ = Animal.prototype • super(name) → Animal.call(this, name) • constructor → the constructor function itself PROOF: Dog.prototype.bark // the method lives on the prototype typeof Dog === "function" // a class IS a function! d.__proto__ === Dog.prototype // same wiring as before
Takeaway: class is 100% syntactic sugar. There is still no "real" class — just functions and prototypes underneath. Knowing this is why prototype questions are an interview favourite.
9. Built-in Prototypes (Array, Object, String)
Every built-in type has a prototype holding its methods:
| You write | Method actually lives on |
|---|---|
| [1,2].map(...) | Array.prototype.map |
| "hi".toUpperCase() | String.prototype.toUpperCase |
| (5).toFixed(2) | Number.prototype.toFixed |
| obj.hasOwnProperty(...) | Object.prototype.hasOwnProperty |
| fn.call(...) | Function.prototype.call |
You can add your own methods to built-in prototypes — but you usually shouldn't:
// This WORKS, but is dangerous:
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
// ⚠ WHY IT'S RISKY:
// • You affect EVERY array in the whole app + libraries
// • A future JS version might add a real .last → conflict
// • It can break for...in loops & other code's assumptions
// This is called "monkey patching" — avoid it in production.
10. Prototype Pollution & Best Practices
Prototype pollution is a real security vulnerability where an attacker injects properties into Object.prototype, affecting every object in the app.
┌─────────────────────────────────────────────────────────────┐
│ PROTOTYPE POLLUTION │
├─────────────────────────────────────────────────────────────┤
│ │
│ If untrusted input can set a key like "__proto__", │
│ an attacker can add properties to Object.prototype: │
│ │
│ malicious JSON: { "__proto__": { "isAdmin": true } } │
│ │
│ Now EVERY object appears to have isAdmin === true! │
│ │
│ DEFENCE: │
│ • Validate/sanitize keys from user input │
│ • Use Object.create(null) for maps (no prototype) │
│ • Use Map instead of plain objects for dynamic keys │
│ • Freeze prototypes: Object.freeze(Object.prototype) │
│ │
└─────────────────────────────────────────────────────────────┘
BEST PRACTICES SUMMARY: ✔ Put methods on the prototype (or use class) — not in the constructor ✔ Use Object.getPrototypeOf / setPrototypeOf, not __proto__ ✔ Use hasOwnProperty to check own vs inherited keys ✔ Use Object.create(null) or Map for safe dictionaries ✗ Don't monkey-patch built-in prototypes in production ✗ Don't reassign __proto__ at runtime (it's slow — deopts the engine)
Bonus: Own vs Inherited Properties
Sometimes you need to know if a property is the object's own or came from the prototype chain:
const dog = Object.create({ species: "canine" });
dog.name = "Rex";
dog.hasOwnProperty("name"); // true — own property
dog.hasOwnProperty("species"); // false — inherited from prototype
"species" in dog; // true — `in` checks the WHOLE chain
// Loop only OWN keys:
Object.keys(dog); // ["name"] (own, enumerable only)
for (const k in dog) { ... } // includes INHERITED enumerable keys!
┌────────────────────┬───────────────┬──────────────────┐ │ │ Own only │ Includes chain │ ├────────────────────┼───────────────┼──────────────────┤ │ hasOwnProperty │ ✔ checks own │ │ │ `in` operator │ │ ✔ whole chain │ │ Object.keys() │ ✔ own enum. │ │ │ for...in │ │ ✔ inherited enum │ └────────────────────┴───────────────┴──────────────────┘
Interview Questions — Quick Fire!
Q: What is a prototype in JavaScript?
"Every JavaScript object has a hidden link to another object called its prototype. When you access a property that doesn't exist on the object, JavaScript looks for it on the prototype, and then up the prototype chain. It's how objects share behaviour without copying it — for example all arrays share the methods on Array.prototype."
Q: What is the prototype chain?
"It's the series of linked prototype objects JavaScript walks when looking up a property. It checks the object itself, then its prototype, then that prototype's prototype, and so on until the property is found or it reaches null, in which case the result is undefined."
Q: What's the difference between __proto__ and prototype?
"prototype is a property that exists only on constructor functions — it's the object that will become the prototype of instances created with new. __proto__ exists on every object instance and is the actual link to its prototype. So for an instance d of Dog, d.__proto__ === Dog.prototype. The modern way to access it is Object.getPrototypeOf."
Q: What does the `new` keyword do?
"Four things: it creates a fresh empty object, links that object's prototype to the constructor function's prototype, runs the constructor with this bound to the new object, and returns the new object unless the constructor explicitly returns its own object."
Q: Why should methods go on the prototype instead of inside the constructor?
"If you define a method inside the constructor, every instance gets its own copy of that function, wasting memory. Putting it on the prototype means all instances share a single function through the prototype chain. The rule of thumb is: instance-specific data on this, shared behaviour on the prototype."
Q: Is a class in JavaScript a real class?
"No — class is syntactic sugar over prototypes. Its methods are placed on the prototype, extends sets up the prototype chain between the two prototypes, and super calls the parent constructor. Under the hood a class is still a function and the wiring is identical to prototypal inheritance. typeof MyClass is even 'function'."
Q: How does prototypal inheritance work?
"You link one prototype to another so an object can access properties from a parent prototype. With constructor functions you call the parent constructor with the child's this, set the child prototype to Object.create(parentPrototype), and fix the constructor reference. Then instances inherit the parent's methods through the chain. With class you just use extends and super."
Q: What does Object.create do?
"Object.create(proto) makes a new object whose prototype is exactly the object you pass in, without needing a constructor function. It's the purest form of prototypal inheritance. Object.create(null) is a special case that makes an object with no prototype at all — useful as a clean dictionary with no inherited keys."
Q: What's the difference between hasOwnProperty and the `in` operator?
"hasOwnProperty returns true only if the property is defined directly on the object itself, ignoring the prototype chain. The in operator returns true if the property exists anywhere in the chain, including inherited properties. Similarly Object.keys and for...in differ: Object.keys returns only own enumerable keys, while for...in also includes inherited enumerable keys."
Q: What is prototype pollution?
"It's a security vulnerability where an attacker injects properties into Object.prototype — often through a malicious key like __proto__ in untrusted JSON — so every object in the app appears to have those properties. You defend against it by validating input keys, using Object.create(null) or Map for dynamic key/value data, and avoiding unsafe deep merges of user input."
Q: Why shouldn't you modify built-in prototypes?
"Because it affects every object of that type across your whole app and any libraries, can clash with future language features, and can break assumptions like for...in loops. This 'monkey patching' is risky in production — it's better to use standalone utility functions or subclasses."
Quick Recap
| Concept | Key Takeaway |
|---|---|
| Prototype | Hidden link every object has to another object it inherits from. |
| Prototype chain | Lookup walks up prototypes until found or null. |
| __proto__ | The instance's actual link to its prototype. |
| prototype | Property on functions; becomes instances' __proto__. |
| new | Creates object, links proto, runs constructor, returns it. |
| Methods on prototype | Shared once, not copied per instance. |
| Inheritance | Chaining prototypes together. |
| Object.create | New object with a chosen prototype. |
| class | Pure syntactic sugar over prototypes. |
| hasOwnProperty | Own key vs inherited (`in` checks the chain). |
| Prototype pollution | Security risk — sanitize keys, use Map/Object.create(null). |
Final Thoughts
Prototypes are the backbone of JavaScript's object model. Once you see that:
- Every property lookup walks a chain,
newjust wires up that chain,- and
classis only sugar over it,
...the entire language clicks into place. This is one of THE most asked JavaScript interview topics — so make sure you can draw the chain diagram from memory and explain __proto__ vs prototype in one clear sentence.
Keep coding, keep learning! See you in the next one!
Post a Comment