Prototypes and Inheritance in JavaScript
How every object links to a prototype, how property lookup walks that chain, what new and class really do, and how prototype pollution turns it into a bug.
Every JavaScript object has a hidden link, [[Prototype]], to another object or to null. Reading a property the object does not have walks that link, then the next one, until it finds the property or reaches null. Inheritance in JavaScript is nothing more than that walk: new and class just build the chain for you, and a writable Object.prototype is why one bad merge can change every object in the process.
Context
Java and C++ share behaviour through classes: a class is a blueprint, and an object is stamped from it. When Brendan Eich designed JavaScript in 1995 he took a different model from the language Self: there are no blueprints, only objects, and an object can delegate to another object it is linked to. To look familiar next to Java, the language added constructor functions and the new keyword on top. ES5 (2009) exposed the link directly with Object.create and Object.getPrototypeOf; ES2015 added class syntax, which still compiles down to the same links; and ES2022 added class fields and #private members.
You use the chain every time you call [1, 2].map(): the array has no map of its own, it finds it on Array.prototype. You see it in DevTools as the [[Prototype]] row under every object, and you met its dark side if you ever patched a dependency for a "prototype pollution" advisory.
const animal = {eats: true, describe() { return 'eats: ' + this.eats }}
const rabbit = Object.create(animal) // rabbit.[[Prototype]] = animal
rabbit.jumps = true
rabbit.jumps // true (own property)
rabbit.eats // true (found on animal)
rabbit.describe() // 'eats: true' (this is still rabbit)
Object.hasOwn(rabbit, 'eats') // false
'eats' in rabbit // true (in checks the whole chain)- [[Prototype]]
- The internal link from an object to the object it delegates to. Read it with Object.getPrototypeOf(obj); DevTools shows it as [[Prototype]].
- .prototype
- An ordinary property that functions (and classes) have. It is the object that new will use as the [[Prototype]] of the instances it creates. It is not the function’s own prototype.
- Own property
- A property stored on the object itself, as opposed to one found further up the chain. Object.hasOwn(obj, key) (ES2022) tests for it.
- Prototype chain
- The sequence of objects reached by following [[Prototype]] links until null.
- Shadowing
- An own property with the same name as one further up the chain; lookup stops at the own one.
Why it matters
Most JavaScript bugs around classes are really prototype bugs: a method that works on instances but not on a destructured reference, a shared array on a prototype that every instance mutates, an instanceof check that fails across iframes or duplicate package copies, or a deep-merge utility that lets a request body set isAdmin on every object in the server. TypeScript's classes, React's old class components, Node streams and every error subclass all run on this mechanism.
The chain and the two meanings of prototype
The word "prototype" names two different things, and mixing them up is the usual source of confusion. Every object has a [[Prototype]] link. Every ordinary function also has a .prototype property, an object that sits there unused until you call the function with new, at which point it becomes the [[Prototype]] of the new instance.
Reading walks the chain; writing does not. Assigning rex.bark = … creates an own property on rex that shadows the shared one, and every other dog is unaffected. The exception is setters and non-writable properties found up the chain, which intercept or block the write. Methods found on the prototype still run with this set to the object you called them on, which is why one copy of bark serves every instance.
| API | What it does | Use it? |
|---|---|---|
Object.create(p) | New object whose [[Prototype]] is p (or null) | Yes, the direct way to link objects |
Object.getPrototypeOf(o) | Reads the [[Prototype]] link | Yes |
Object.setPrototypeOf(o, p) | Relinks an existing object (ES2015) | Rarely: engines deoptimize the object |
o.__proto__ | Legacy getter/setter on Object.prototype | No: kept only for web compatibility |
Object.hasOwn(o, k) | Own property only (ES2022) | Yes, instead of hasOwnProperty |
What new and class actually do
new F(...args) is four steps, and writing them out is the clearest way to understand it (and a common interview task):
function myNew(Ctor, ...args) {
// 1. a fresh object linked to Ctor.prototype
const obj = Object.create(Ctor.prototype)
// 2. run the constructor with this = obj
const result = Ctor.apply(obj, args)
// 3. if the constructor returned an object, that wins
const isObject = result !== null &&
(typeof result === 'object' || typeof result === 'function')
// 4. otherwise the new object is the result
return isObject ? result : obj
}
function Dog(name) { this.name = name }
Dog.prototype.bark = function () { return this.name + ' says woof' }
const rex = myNew(Dog, 'Rex')
rex.bark() // 'Rex says woof'
Object.getPrototypeOf(rex) === Dog.prototype // true
rex instanceof Dog // trueA class produces the same structure: methods go on Class.prototype, the constructor body runs with this bound to the new instance, and extends links two chains, one for instances and one for the constructors themselves, so static methods inherit too. The differences are real but small: class bodies are strict mode, a class cannot be called without new, methods are non-enumerable, and a subclass must call super() before touching this because the base constructor creates the object.
class Animal {
constructor(name) { this.name = name }
speak() { return this.name + ' makes a sound' }
static create(name) { return new this(name) }
}
class Dog extends Animal {
speak() { return super.speak() + ': woof' }
}
Dog.create('Rex').speak() // 'Rex makes a sound: woof'function Animal(name) { this.name = name }
Animal.prototype.speak = function () {
return this.name + ' makes a sound'
}
Animal.create = function (name) { return new this(name) }
function Dog(name) { Animal.call(this, name) }
// chain 1: instances
Object.setPrototypeOf(Dog.prototype, Animal.prototype)
// chain 2: constructors, so Dog.create exists
Object.setPrototypeOf(Dog, Animal)
Dog.prototype.speak = function () {
return Animal.prototype.speak.call(this) + ': woof'
}Fields live on the instance
Class fields (ES2022), including arrow-function fields and #private members, are created on each instance when the constructor runs. They are not on the prototype, so a thousand instances with an arrow field hold a thousand functions, while a method is shared by all of them. #private names are not properties at all: they are not found by lookup, not inherited through the chain, and cannot be read from outside the class body.
Prototype pollution
Almost every object shares Object.prototype at the top of its chain. If code lets untrusted input choose a property path, and the path is __proto__ (or constructor.prototype), the write lands on Object.prototype and every object in the process inherits the new property. The classic vector is a recursive merge or a "set by path" helper; lodash's defaultsDeep (CVE-2019-10744) is the best-known case.
- 1A request body arrives as JSON:
{"__proto__": {"isAdmin": true}}.JSON.parsecreates an ordinary own key named__proto__, which is harmless so far. - 2A naive deep merge copies it into a config object with
target[key] = …. Assignment totarget['__proto__']goes through the legacy setter and reachesObject.prototype. - 3Now
({}).isAdmin === truefor every object in the process, including the emptyuserobject a later authorization check reads.
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] ??= {}
merge(target[key], source[key]) // follows __proto__
} else {
target[key] = source[key]
}
}
return target
}
merge({}, JSON.parse(body))
;({}).isAdmin // true for every objectconst BLOCKED = new Set(['__proto__', 'constructor',
'prototype'])
function merge(target, source) {
for (const key of Object.keys(source)) { // own keys only
if (BLOCKED.has(key)) continue
const value = source[key]
if (value && typeof value === 'object') {
if (!Object.hasOwn(target, key)) {
target[key] = Object.create(null) // no chain at all
}
merge(target[key], value)
} else {
target[key] = value
}
}
return target
}Defences, from most to least structural: validate request bodies against a schema before they reach generic object code; use Map or Object.create(null) for dictionaries keyed by user input, since neither has Object.prototype on its chain; block the three dangerous keys in any path-based setter; and, in Node, --disable-proto=delete removes the legacy accessor altogether.
Pitfalls
- Mutable state on the prototype
Dog.prototype.tricks = []looks like a default, but every instance reads the same array, andrex.tricks.push()mutates it for all dogs because push reads the shared array instead of writing a new property. Initialise per-instance state in the constructor or as a class field. - Relinking prototypes at runtime
Engines like V8 optimise property access with hidden classes and inline caches that assume an object's shape and chain stay stable.
Object.setPrototypeOfor assigning__proto__on a live object invalidates those assumptions and can make every access to it slow. Build the chain at creation time withObject.createor a class instead. - Extending built-in prototypes
Adding methods to
Array.prototypeorObject.prototypechanges every array or object in the program and can collide with future standard methods. It has happened: the proposedArray.prototype.flattenwas renamed toflatin 2018 because an old library had already defined an incompatible one. Use plain functions or a subclass. - for…in sees inherited properties
for…initerates enumerable properties along the whole chain, so anything added to a prototype (or polluted ontoObject.prototype) shows up in the loop. UseObject.keys,Object.entriesorfor…ofover aMap. - Deep inheritance hierarchies
Each level couples the subclass to the base class's internals, and a change in the middle ripples down. Most JavaScript code is easier to change with composition: plain objects holding the behaviours they need, or small functions, with inheritance kept for genuine "is-a" cases such as error types.
Interview questions
Q1What is prototypal inheritance?
Objects inherit directly from other objects through a [[Prototype]] link instead of from class blueprints. When a property is missing on an object, the engine looks on its prototype, then that object's prototype, until it finds it or hits null. Methods are shared this way: one function on the prototype serves every object linked to it, and runs with this set to the object it was called on.
Q2What is the difference between __proto__ and prototype?
__proto__ is a legacy accessor for an object's own [[Prototype]] link, the object it delegates to; Object.getPrototypeOf is the standard way to read it. prototype is an ordinary property on functions and classes, the object that new will install as the [[Prototype]] of the instances it creates. So Object.getPrototypeOf(rex) === Dog.prototype, while Dog's own [[Prototype]] is Function.prototype.
Q3Walk me through implementing the new operator yourself.
Create an object linked to the constructor's prototype with Object.create(Ctor.prototype), call the constructor with apply so this is that object, and return the constructor's result if it is an object or function, otherwise the new object. That covers constructor functions; classes add a guard that throws when called without new, and derived classes let the base constructor create the object, which is why super must run first.
Q4Are JavaScript classes real classes?
They are syntax over the same prototype mechanism, not a separate class model. Methods still live on Class.prototype, instances still delegate to it, and extends links prototypes. The syntax adds real guarantees, though: strict mode, no call without new, non-enumerable methods, super, static inheritance, fields and truly private #members, which constructor functions cannot express.
Q5What happens when you assign to a property that exists only on the prototype?
Normally the assignment creates a new own property on the object, shadowing the prototype's, and the prototype is untouched. Two exceptions: if the inherited property is an accessor, its setter runs instead, and if it is non-writable the assignment silently fails, or throws in strict mode. Mutating an inherited object, like pushing to a shared array, is different: that is a read followed by a mutation, so it changes the shared value.
Q6What is prototype pollution and how do you prevent it?
It is when untrusted input writes through __proto__ or constructor.prototype into Object.prototype, so every object in the process gains attacker-chosen properties, often enough to bypass an authorization check or reach code execution through a gadget. I prevent it by validating input against a schema, using Map or Object.create(null) for user-keyed data, skipping those keys in any merge or set-by-path helper, and keeping dependencies with known merge bugs patched.
Q7Why can instanceof return false for an object that clearly is an array?
instanceof checks whether Array.prototype from the current realm is on the object's chain. An array created in another iframe, a worker message or a vm context has a different Array.prototype, so the check fails. The same happens with a class loaded from two copies of a package. Array.isArray and brand checks work across realms.
- Every object has a [[Prototype]] link; a missing property is looked up along that chain until null.
- A function's .prototype property is not its own prototype: it is what new installs as the [[Prototype]] of new instances.
- new links a fresh object to Ctor.prototype, runs the constructor with that object as this, and returns it unless the constructor returns an object.
- class is syntax over prototypes with real extras: strict mode, required new, super, static inheritance, per-instance fields and #private members.
- Reads walk the chain, writes create own properties; mutable values on a prototype are shared by every instance.
- Never let untrusted keys reach Object.prototype: validate input, use Map or Object.create(null), and block __proto__, constructor and prototype in merges.