Prototype Inheritance Bug — Shared Array Leaks Tenant Data
A shared array on a prototype caused cross-tenant data leakage.
- Prototype chain: each object has a hidden link to another object — property lookup walks up the chain automatically
- __proto__ vs .prototype: __proto__ is instance pointer, .prototype is constructor property that becomes the prototype of instances
- Memory efficiency: one function on prototype saves thousands of copies — ~40 bytes per instance avoided
- Production gotcha: mutating a shared array on prototype affects all instances — use instance properties for mutable state
- Biggest mistake: thinking ES6 classes changed the system — they're still prototypes under the hood
JavaScript is one of the few mainstream languages where inheritance isn't bolted on through classes in the traditional sense — it's baked into the very fabric of how objects work. Every single object you create, from a plain {} to a complex class instance, is silently connected to a chain of other objects. Understanding that chain isn't just academic trivia — it's the reason Array methods like .map() and .filter() exist on every array you've ever written without you defining them yourself.
Before ES6 classes arrived, the only way to share behaviour between objects was through prototypes directly. Classes are still prototypes underneath — they're syntactic sugar, not a different system. If you don't understand the prototype chain, you'll hit confusing bugs when properties seem to appear from nowhere, or when methods you thought were isolated suddenly affect every object in your app.
By the end of this article you'll be able to explain exactly what __proto__ and prototype mean and why they're different, build an inheritance hierarchy two ways (prototype-based and class-based) and know the trade-offs, spot and fix the most common prototype bugs, and answer the prototype questions that trip people up in senior JavaScript interviews.
What the Prototype Chain Actually Is — And Why JS Needs It
Every JavaScript object has an internal slot called [[Prototype]]. Think of it as a hidden pointer that says 'if you can't find a property on me, go look over there.' That 'over there' is another object — its prototype — which has its own [[Prototype]], and so on, until you reach Object.prototype, whose [[Prototype]] is null. That's the end of the chain.
This design solves a memory problem. If you create 10,000 User objects and each one stored its own copy of the greet() method, you'd have 10,000 identical functions in memory. With the prototype chain, you define greet() once on User.prototype and all 10,000 instances share a single reference to it. The method lives in one place; the lookup walks there automatically.
The browser exposes this via __proto__ (the instance's pointer to its prototype) and .prototype (a property on constructor functions that becomes the [[Prototype]] of objects they create). These are different things and mixing them up is the source of enormous confusion — we'll untangle that in the code below.
Property lookup always starts on the object itself. Only if the property isn't found there does JS walk up the chain. This means instance properties always shadow prototype properties — a critical detail when debugging unexpected values.
Constructor Functions and .prototype — Building Inheritance Before ES6
Before ES6 classes, JavaScript developers used constructor functions to create objects with shared behaviour. A constructor function is just a regular function you call with the new keyword. When you do that, JavaScript does four things automatically: creates a new empty object, sets its [[Prototype]] to the constructor's .prototype property, runs the function body with this pointing to the new object, and returns that object.
This is where the .prototype property on functions becomes important. Every function in JavaScript automatically gets a .prototype object. When you add a method to that .prototype, every instance created by that constructor gets access to it through the chain — without storing their own copy.
The pattern for inheritance between two constructor functions requires one extra step: wiring up the prototype chain manually with Object.create(). If you forget this, instances of the child constructor won't be able to reach the parent's methods. This manual wiring is exactly what ES6 classes automate with the extends keyword — the underlying mechanism is identical.
Understanding this pattern isn't just history — you'll encounter it in older codebases regularly, and knowing it makes ES6 class behaviour completely transparent.
ES6 Classes — Same Prototype Chain, Cleaner Syntax
ES6 classes aren't a new object system. They're a cleaner way to write the exact same constructor-function pattern you just saw. Under the hood, class produces a constructor function, and extends sets up the prototype chain using Object.create() just like we did manually. Don't let the keyword fool you into thinking JavaScript became a classical OOP language — it didn't.
The super keyword in a child class does two jobs. Inside the constructor, super(...args) calls the parent's constructor function — the equivalent of Animal.call(this, ...) that we wrote manually. Inside a method, super.methodName() climbs up the prototype chain and calls the parent's version of that method, which is how you extend behaviour rather than replace it.
One genuinely new thing classes give you is private fields (using the # prefix), which the prototype pattern never had. Private fields are stored directly on the instance and are invisible outside the class body — they're not on the prototype at all, which is why they can't be accessed via the chain.
Knowing both syntaxes is what makes you dangerous. You can read legacy code, you can explain what a transpiler like Babel actually outputs, and you'll never be confused by a class behaving 'weirdly' because you understand the prototype reality underneath.
Real-World Pattern — Composable Mixins Over Deep Inheritance
Here's the dirty secret senior engineers know: deep inheritance hierarchies are fragile. The moment your requirements change, the base class is impossible to modify without breaking every subclass. This is the 'gorilla banana problem' — you wanted a banana, but you inherited the gorilla holding it and the entire jungle it lives in.
The JavaScript prototype chain gives you a practical alternative: mixins. Instead of inheriting from a chain of parent classes, you compose objects by copying or delegating methods from multiple sources. This is idiomatic modern JavaScript and it sidesteps the rigidity of single-parent inheritance entirely.
The most common mixin pattern uses Object.assign() to copy methods onto a prototype, or uses a function that takes a superclass and returns an extended class (a 'class factory mixin'). This lets you snap in capabilities like Serializable, Timestamped, or Auditable without creating a brittle hierarchy.
This matters in the real world because UI component libraries, state management systems, and ORMs all use composition patterns like this. Recognising them — and knowing when to choose composition over inheritance — is what separates intermediate developers from senior ones.
Prototype Pollution: The Security Vulnerability You Didn't Know You Had
Prototype pollution is a JavaScript vulnerability where an attacker injects properties into Object.prototype. Since every object inherits from Object.prototype, a single injection can affect all objects in the runtime. This typically happens when recursive merge functions or unsafe JSON parsing overwrite __proto__ or constructor.prototype.
The attack surface is any code that merges user-controlled objects without sanitisation. Libraries like lodash had CVEs for this. The fix is to use Object.create(null) for dictionaries, sanitise property keys, or freeze Object.prototype in high-security environments.
This isn't just a theoretical attack — real breaches have happened via prototype pollution in client-side frameworks and server-side Node.js applications. Understanding the prototype chain is the only way to fully grasp why pollution works and how to prevent it.
| Aspect | Constructor Functions (.prototype) | ES6 Classes (class / extends) |
|---|---|---|
| Syntax clarity | Verbose — chain wiring is manual | Clean — extends and super handle it |
| Prototype chain | Identical underlying mechanism | Identical underlying mechanism |
| Private state | Requires closure workaround | Native private fields with # prefix |
| Calling parent constructor | Animal.call(this, ...args) | super(...args) — must be first |
| Calling parent method | Animal.prototype.method.call(this) | super.method() |
| Hoisting | Function declarations are hoisted | Classes are NOT hoisted — use after declaration |
| Strict mode | Optional | Always in strict mode automatically |
| Seen in codebases | Legacy and transpiled output (Babel) | Modern JS, TypeScript, frameworks |
Key Takeaways
- The prototype chain is a linked list of objects — property lookup walks up it automatically, which is why .map() exists on every array without you defining it.
- Constructor function .prototype and instance __proto__ are different things: .prototype is a property on functions that becomes the [[Prototype]] of instances; __proto__ (or Object.getPrototypeOf) is how you read that link on an instance.
- ES6 classes compile down to the same constructor-function + prototype pattern — 'extends' runs
Object.create()and 'super()' runs the parent constructor. The chain is identical. - Deep inheritance hierarchies are fragile — class factory mixins let you compose reusable abilities (Serialisable, Timestamped) onto unrelated classes without forcing them into a single hierarchy.
- Prototype pollution is a real security threat — understanding the chain is the first step to preventing it.
Common Mistakes to Avoid
- Adding methods directly inside the constructor (this.method = function…)
Symptom: Every instance carries its own copy of the function, wasting memory. Debug by checking instance.hasOwnProperty('method') returns true.
Fix: Move methods to the prototype: ClassName.prototype.method = function… or use ES6 class syntax where methods are automatically on the prototype. - Forgetting to call super() before using 'this' in a derived class
Symptom: ReferenceError: Must call super constructor in derived class before accessing 'this'
Fix: Always call super(...args) as the first statement in the child class constructor. JavaScript enforces this because the parent constructor initializes the object. - Mutating a shared array or object on the prototype
Symptom: Pushing to one instance's array updates every instance's array — looks like a ghost-write bug.
Fix: Initialize arrays and objects inside the constructor (this.items = []) so each instance gets its own copy. Prototypes should only hold methods, never mutable state. - Using Dog.prototype = Animal.prototype instead of Object.create
Symptom: Adding a method to Dog.prototype also adds it to Animal.prototype — because they're the same object.
Fix: Use Dog.prototype = Object.create(Animal.prototype) to create a new object that delegates to Animal.prototype. - Forgetting to reset constructor after Object.create
Symptom: instance.constructor points to the parent class, not the child. Breaks code that relies on constructor for type checks.
Fix: After setting prototype, add: Child.prototype.constructor = Child;
Interview Questions on This Topic
- QWhat is the difference between __proto__ and .prototype in JavaScript? Can you draw out the relationship between a constructor function, its .prototype property, and an instance created from it?JuniorReveal
- QWhat does the 'new' keyword actually do step by step? If you had to implement your own version of 'new' as a function, how would you write it?Mid-levelReveal
- QES6 classes are often called 'syntactic sugar over prototypes' — do you agree with that statement entirely, or are there things classes do that the old constructor-function pattern genuinely cannot?SeniorReveal
- QExplain prototype pollution — what is it, how does it work, and how do you prevent it?SeniorReveal
Frequently Asked Questions
What is the prototype chain in JavaScript?
The prototype chain is the mechanism JavaScript uses to look up properties and methods. Every object has a hidden [[Prototype]] link to another object. When you access a property, JS first checks the object itself, then walks up the chain through each linked object until it either finds the property or reaches null at the end of the chain.
What is the difference between classical inheritance and prototypal inheritance?
Classical inheritance (Java, C++) copies a blueprint — the class — into new instances at compile time. Prototypal inheritance (JavaScript) links objects together at runtime. Instances don't copy methods; they delegate to their prototype. This means you can modify the prototype after instances are created and all instances immediately see the change.
Does using ES6 classes mean JavaScript is no longer prototype-based?
No. ES6 classes are syntax on top of the same prototype system. Running class Dog extends Animal creates a constructor function and wires Dog.prototype to Animal.prototype using Object.create() — exactly as you'd do manually. You can verify this by checking Object.getPrototypeOf(Dog.prototype) === Animal.prototype, which returns true for any class hierarchy.
What is the best way to inspect an object's prototype chain?
Use Object.getPrototypeOf() in a loop until null. Or in browser DevTools, console.dir(obj) shows the full chain. Avoid __proto__ in production code.
How do I prevent prototype pollution in my application?
Use Object.create(null) for objects that serve as maps, always filter __proto__ and constructor keys when merging user input, freeze Object.prototype in critical environments, and use libraries with built-in pollution prevention (e.g., lodash's cloneDeep has patches but still sanitize yourself).
That's Advanced JS. Mark it forged?
5 min read · try the examples if you haven't