The Prototype Chain
Prerequisites: this chapter assumes you already know objects, property access, functions, and this binding, and are comfortable reading ordinary JavaScript. You do not need prior knowledge of class syntax; we build up to it.
The core mechanism
Every object in JavaScript has an internal link to another object, its prototype. When you read obj.prop, the engine first checks obj's own properties. If the property is absent, it follows that internal link and checks the prototype object, then its prototype, and so on, until it either finds the property or reaches the end of the chain (null). This is the prototype chain.
The internal link is not a normal property. It is exposed through Object.getPrototypeOf(obj) (read) and Object.setPrototypeOf(obj, proto) (write, slow and best avoided). Objects created with literal syntax have Object.prototype as their prototype.
const obj = { a: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null — end of chain
console.log(obj.toString); // function — found on Object.prototype, not on obj
console.log('a' in obj); // true (own or inherited)
console.log(Object.hasOwn(obj, 'a')); // true (own only)Property writes behave differently: assignment to obj.prop = x always creates or updates an own property on obj (subject to setters on the prototype, discussed below). Reads walk the chain; writes do not.
Constructor functions and `.prototype`
Functions get a second role. Any regular function has a .prototype property — an object that becomes the prototype of objects created by calling the function with new.
function Point(x, y) {
this.x = x; // own property, per instance
this.y = y;
}
Point.prototype.dist = function () { // shared by all instances
return Math.hypot(this.x, this.y);
};
const p = new Point(3, 4);
console.log(Object.hasOwn(p, 'x')); // true
console.log(Object.hasOwn(p, 'dist')); // false — inherited
console.log(Object.getPrototypeOf(p) === Point.prototype); // trueThe design consequence: state that varies per instance goes on the instance in the constructor; behavior shared by all instances goes on the constructor's .prototype. Methods stored per-instance cost memory and defeat identity comparison, a common mistake.
Two more pieces:
- Every function object inherits from
Function.prototype(which gives youcall,apply,bind), andFunction.prototypeinherits fromObject.prototype. So methods exist two chains deep on functions. - The
.constructorproperty you often see on instances is not special. It is just an inherited, writable data property ofFunction.prototype's default.prototypeobject. Overwrite.prototypewith a fresh object and you lose it unless you restore it.
Point.prototype = { dist() { return 0; } };
console.log(new Point(1, 2).constructor === Point); // false
Point.prototype.constructor = Point; // conventional repair`class` is syntax over the same chain
A class declaration creates a function whose .prototype holds its methods. extends sets up a chain between the two prototype objects and between the two constructors themselves (so static methods inherit too), and wires up super.
class Shape {
area() { return 0; }
}
class Circle extends Shape {
constructor(r) { super(); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
const c = new Circle(1);
console.log(Object.getPrototypeOf(c) === Circle.prototype); // true
console.log(Object.getPrototypeOf(Circle.prototype) === Shape.prototype); // true
console.log(Object.getPrototypeOf(Circle) === Shape); // statics chain tooclass is not a new object model; it is enforced discipline over the one above: methods are non-enumerable, classes must be called with new (no implicit this), and the chain is immutable.
Gotchas and failure modes
- Mutating shared prototypes. Adding to
Object.prototypeorArray.prototypepollutes every object in the program, including library code. Avoid it; if you must (polyfills), useObject.definePropertywithenumerable: false. for...inwalks inherited enumerable properties. PreferObject.keys/Object.entries(own keys only), or guard withObject.hasOwn.- Shadowing. Assigning
c.area = ...creates an own property that hides the inherited one. The inherited method still exists on the prototype; other instances are unaffected. This is per-instance override, not mutation of the prototype. - Setters intercept writes. If a prototype has a setter for
prop, thenobj.prop = vinvokes that setter instead of creating an own property — even though reads fall through to the prototype. UseObject.defineProperty(obj, ...)to force an own data property. - Performance. Very long chains and
setPrototypeOfdefeat the engine's inline caches. Shapes that access the same properties through stable chains are fast; polymorphic, reshaped chains are not. __proto__vsprototype.obj.__proto__(legacy, deprecated but widely supported) is the instance's prototype link.Func.prototypeis the template for instances ofFunc. They are different things and confusion between them is the classic beginner error; at this level, always reach forObject.getPrototypeOf.
Decision criteria
Use plain objects with the literal prototype when you have data with a handful of shared methods. Use class when you have a real type hierarchy with instanceof and super needs. Use closure-based factories (no prototype at all) when you want true privacy or per-instance method identity — accepting the memory cost. Avoid Object.setPrototypeOf in hot code, and avoid deep chains beyond two or three levels: they are hard to debug and slow to optimize.
Exercise
Given const a = { greet() {} }; const b = Object.create(a); b.greet = function () {}; — does assigning b.greet change which function a.greet is, or what other objects see?
Show answer
No. The assignment creates an own greet property on b that shadows the inherited one. a.greet is untouched, and any other object with a in its chain still resolves to a's original method.
Exercise
Why does typeof Circle === 'function' and Object.getPrototypeOf(Circle) === Shape hold for classes, and what does the second fact give you?
Show answer
A class declaration compiles to a constructor function, so it is a function object. extends sets the class's own prototype (the static chain) to the superclass, so static members and Circle.__proto__-level lookups inherit from Shape — this is how static inheritance works.
Spotted a mistake, or code that does not work?