Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works

What will I learn
- You will learn the full prototype chain, from an object up to
Object.prototypeand finallynull; - exactly how JavaScript resolves a property lookup, step by step;
- why every plain object inherits methods like
toStringandhasOwnProperty; - what
Object.create(null)gives you, and when a chain-free object is genuinely useful; - how the chain relates to performance and to the built-in types you use every single day.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-34 read, especially the prototype (ep34) which this episode builds directly on.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works (this post)
Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
Last episode we cracked open the single most distinctive feature of JavaScript's object model: the prototype, that hidden link from one object to another. We saw a dog borrow a describe method from an animal, and we watched arrays quietly inherit map from Array.prototype. But I left one thread deliberately dangling, and it is the thread that ties the whole thing together. That prototype link does not point at some magical final object. It points at an object which has its own prototype link, pointing at yet another object, and so on. This series of links is the prototype chain, and today we follow it all the way to the end.
If you get this episode, you will never again be surprised by where a method "comes from" in JavaScript. You will be able to look at any value -- an object, an array, a function, a string -- and predict exactly which object holds each method it can call, and in what order the engine searches. Having said that, let's earn it properly, from the ground up. ;-)
Solutions to Episode 34 Exercises
Exercise 1 - inheriting a method:
const vehicle = { start() { return `the ${this.kind} starts`; } };
const car = Object.create(vehicle);
car.kind = "car";
console.log(car.start()); // "the car starts"
console.log(Object.hasOwn(car, "start")); // false - inherited, not own
The insight: start is found on the prototype vehicle, but this is car (the object before the dot at the call site), so this.kind reads "car". The method lives in one place, the data lives per object.
Exercise 2 - shadowing an inherited method:
const truck = Object.create(vehicle);
truck.kind = "truck";
truck.start = () => "custom truck start"; // own property shadows the inherited one
console.log(truck.start()); // "custom truck start"
console.log(vehicle.start.call({ kind: "x" })); // "the x starts" - prototype untouched
The insight: assigning start creates an own property that shadows the inherited method; the prototype's version is completely unchanged, as we proved by borrowing it with .call.
Exercise 3 - array methods live on the prototype:
const arr = [1, 2, 3];
console.log(Object.hasOwn(arr, "filter")); // false
console.log(arr.filter === Array.prototype.filter); // true
The insight: filter is inherited from Array.prototype, so a million arrays share one single function object -- there are no per-array copies. That is the whole economy of the prototype model in one line.
Right, solutions behind us. Now let's follow the chain all the way up and see exactly how a lookup resolves.
Lookup, step by step
Last episode we saw that reading a missing property follows the prototype link. But as I said in the intro, that link points to an object, which has its own prototype, which has its own, and so on. Property lookup walks this chain from the bottom upward until it either finds the property or runs out of chain entirely.
Here is the precise algorithm JavaScript runs every time you read obj.someProperty. It is worth memorising, because it explains an enormous amount of behaviour that otherwise looks like magic:
- Does
objhave an own propertysomeProperty? If yes, use that value. Stop immediately. - If not, go to
obj's prototype and repeat step 1 there. - Keep climbing, prototype to prototype, checking own properties at each level.
- If you reach the end of the chain (
null) without ever finding it, the result isundefined.
Notice what step 1 says: the search stops the instant it finds the property. It does not keep going to see if a "better" one exists higher up. The closest one wins, exactly like variable shadowing in nested scopes back in episode 10. Let's build a three-level chain and watch a lookup climb it:
const grandparent = { surname: "the great" };
const parent = Object.create(grandparent);
parent.role = "mentor";
const child = Object.create(parent);
child.name = "scipio";
console.log(child.name); // "scipio" - own property, found at level 0
console.log(child.role); // "mentor" - found on parent, level 1
console.log(child.surname); // "the great" - found on grandparent, level 2
console.log(child.missing); // undefined - not found anywhere in the chain
Each lookup starts at child and climbs only as far as it must. name is found immediately (zero hops), role one level up, surname two levels up, and missing is never found, so after the engine exhausts the chain it yields undefined -- note, undefined, not an error. That is the entire mechanism: a plain linear search up a chain of objects. No blueprints, no classes, no compilation step. Just objects pointing at objects.
There is a small but important consequence hiding in that last line. A missing property returns undefined rather than throwing, which is why a typo like child.nmae fails silently and gives you undefined down the line, instead of a loud error at the point of the mistake. Quit some beginner bugs live in exactly that gap, so when a value is mysteriously undefined, "did I spell the property right, and is it actually on the chain?" is a great first question to ask.
The chain always ends at Object.prototype, then null
So where does the climbing stop for a normal object? Every ordinary object's chain eventually reaches one special object called Object.prototype, the common ancestor of nearly all objects in the language. This is the object that provides the methods every object seems to "just have" for free: toString, hasOwnProperty, valueOf, isPrototypeOf, and a handful of others. And directly above Object.prototype sits null -- the very top of the chain, the engine's signal that the search is over and there is nowhere left to climb:
const obj = { a: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null - the top
// this is why every object has toString, even an empty one:
console.log({}.toString()); // "[object Object]" - from Object.prototype
console.log(obj.hasOwnProperty); // [Function] - also from Object.prototype
So a plain object like { a: 1 } has a beautifully short two-link chain: itself, then Object.prototype, then null. That is precisely why toString and hasOwnProperty are available on every object you ever create -- they live at the top, on Object.prototype, and every object inherits from it by default. When you called hasOwnProperty in earlier episodes, or when the console printed [object Object] for you, you were quietly using this shared ancestor without knowing it.
This also finally explains a detail from episode 34. Remember I warned that the legacy __proto__ is "an inherited accessor living on Object.prototype"? Now you can see literally where it lives -- it is one of the members on that top object, which is exactly why every object appears to have it. The chain is not an abstraction; it is a real, inspectable set of objects you can poke at from the console.
Chains for built-in types
Built-in types simply add an extra link near the bottom. An array's chain runs: the array itself, then Array.prototype (where map, filter, push, slice and friends live), then Object.prototype, then null. That structure is exactly why an array has both array-specific methods and the generic object methods:
const arr = [1, 2, 3];
// chain: arr -> Array.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(arr.map); // [Function: map] - from Array.prototype (one hop)
console.log(arr.toString); // [Function: toString] - from Object.prototype (two hops)
The identical pattern holds for every built-in. Functions link through Function.prototype, plain wrapped strings through String.prototype, dates through Date.prototype, and each of those prototypes ultimately links up to Object.prototype. So when you write [1,2,3].map(...), the engine fails to find map on your array, hops to Array.prototype, finds it there, and runs it with this bound to your array. It is the same dog.describe() story from last episode, just with the standard library playing the role of the shared prototype. Once you internalise this, you can predict where any method a value has actually lives, which makes reading MDN and reading stack traces dramatically less mysterious.
Here is a fun corollary that trips people up in interviews. Because array methods live on the shared Array.prototype, you can borrow them for array-like objects (things with a length and numeric keys, but no array methods of their own) using .call, which we studied in episode 23:
function collect() {
// 'arguments' is array-like, not a real array - it has no .map of its own
return Array.prototype.map.call(arguments, (x) => x * 2);
}
console.log(collect(1, 2, 3)); // [2, 4, 6]
We reached up into Array.prototype, grabbed map, and pointed its this at something that was never linked to Array.prototype at all. The method does not care what object it lives near -- it only cares what this is at call time. That is delegation and explicit this-binding working hand in hand.
in versus hasOwn, revisited
Now the chain lets us pin down the two membership checks with total precision, and this is a distinction I want you to nail because it causes real bugs. Object.hasOwn(obj, key) returns true only if the property is an own property (found at level 0, on the object itself). The in operator returns true if the property is found anywhere in the chain. So they agree for own properties and disagree exactly for inherited ones:
const arr = [1, 2, 3];
console.log(Object.hasOwn(arr, "length")); // true - length is an OWN property of the array
console.log(Object.hasOwn(arr, "map")); // false - map is inherited from Array.prototype
console.log("map" in arr); // true - 'in' walks the chain and finds it
length is genuinely an own property of the array (it changes as you push and pop), but map is inherited, so hasOwn says false while in says true. Two different questions -- "is this the object's own data?" versus "can this object respond to this key at all?" -- and two different tools. Reach for Object.hasOwn (the modern replacement for obj.hasOwnProperty, available since 2022) whenever you mean "just this object's own stuff".
This is also the deeper reason we were wary of for...in back in episode 8. for...in walks the chain and visits inherited enumerable properties too, which can surprise you if a prototype up the line carries enumerable data. That is why, for iterating an object's own keys, we consistently prefer Object.keys (own enumerable only) or a guarded for...in with a hasOwn check inside. Same lesson, but now you can see the machinery that makes the guard necessary:
const base = { inherited: "from base" };
const own = Object.create(base);
own.mine = "on own";
for (const key in own) console.log("for...in:", key); // "mine" AND "inherited"
console.log(Object.keys(own)); // ["mine"] - own enumerable only
A quick look sideways: how other languages resolve a lookup
Because quite a few of you arrived here from the Learn Python Series (or know some Rust or Go), the contrast is genuinely illuminating -- and it reveals that the prototype chain is not as exotic as it first looks.
In Python, a method call on an instance also walks a chain, it is just a chain of classes rather than a chain of plain objects. Python looks on the instance first, then on its class, then on each base class in the famous MRO (method resolution order). Structurally that is the same "check here, not found, climb up, check again" dance JavaScript does:
class Animal:
def describe(self):
return f"I am a {self.kind}"
class Dog(Animal): # Dog -> Animal -> object (the MRO chain)
pass
d = Dog()
d.kind = "dog"
print(d.describe()) # found on Animal, one step up the class chain
print(Dog.__mro__) # (Dog, Animal, object) - Python's version of the chain
The difference is philosophical: Python bakes the chain into a formal class/instance distinction with a fixed MRO computed up front, whereas JavaScript exposes the raw chain of plain objects, with no privileged "class" entity at the bottom -- and you can even re-wire it at runtime. Go takes a completely different road: it has no inheritance chain at all, preferring struct embedding and interfaces, so a method set is assembled by composition, not by climbing ancestors. Rust likewise has no inheritance; it resolves methods through traits and their implementations at compile time. So the universal need -- "let many values share one implementation" -- gets four different answers, and JavaScript's answer is the live, inspectable chain of objects we are walking today. Understanding that, the language stops feeling weird and starts feeling coherent.
Object.create(null): an object with no chain
Occasionally you want an object with no prototype at all -- a completely bare object that inherits nothing, not even toString or hasOwnProperty. You get exactly that with Object.create(null). Such an object is a pure key-value store with an empty chain: itself, then straight to null.
const bare = Object.create(null);
bare.name = "scipio";
console.log(bare.name); // "scipio"
console.log(Object.getPrototypeOf(bare)); // null - no chain at all
console.log(bare.toString); // undefined - it did NOT inherit toString!
Why on earth would you want an object that is missing the conveniences every other object has? Because those conveniences can collide with your data. A bare object is safe to use as a dictionary of arbitrary, possibly-untrusted keys, precisely because it has no inherited properties for your keys to clash with. Watch the trap that a normal object walks straight into:
const normal = {};
console.log("toString" in normal); // true - inherited, even though you never added it!
console.log("hasOwnProperty" in normal); // true - also inherited
const dict = Object.create(null);
console.log("toString" in dict); // false - nothing inherited, so keys are purely YOUR data
dict["toString"] = "a real value"; // safe: no collision with an inherited method
console.log(dict.toString); // "a real value"
With a normal object, a user-supplied key like "toString" or the notorious "__proto__" can collide with inherited members and cause genuinely nasty bugs (and even security holes -- prototype pollution, which we will study properly much later in the security phase). A null-prototype object has no such members, so every key is unambiguously your own data. For general-purpose use a real Map (episode 71) is usually the better and more explicit choice, but Object.create(null) remains the classic lightweight "clean dictionary" you will see all over serious codebases.
Chains and performance
A brief word on the machine, since we care about how this actually runs (Phase 7 goes deep on the engine). Property lookup on a long chain is, in principle, slower than finding an own property, because the engine may have to walk several links before it succeeds -- or walk the entire chain to conclude a property is absent and return undefined. That "all the way to null" walk for missing properties is the worst case, and it is one reason blindly probing for properties that usually are not there can be a subtle cost.
In practice, though, modern engines optimise this heavily with inline caches (episode 105): once the engine has seen that someObject.foo was found two links up, it remembers the shape and the location, so the next lookups are effectively as fast as a direct hit. Two habits keep those optimisations working for you. First, keep prototype chains shallow rather than deeply nested -- three or four levels is plenty for almost any design, and each extra level is another potential hop. Second, and this is the big one from last episode, never mutate an object's prototype after creation with setPrototypeOf; doing so forces the engine to throw away the caches it built for that object, and it is famously slow. Set the prototype once, at birth (with Object.create, our next topic), and let the engine cache lookups to its heart's content. Do that, and the chain gives you powerful sharing at effectively negligible cost.
Try it yourself
- Build a three-level chain (
base->middle->leaf) withObject.create, putting a different property on each level. Read a property from each level throughleaf, and read a non-existent property to seeundefined. For each read, say how many hops up the chain the lookup travelled. - Take a plain object and prove its chain is
obj -> Object.prototype -> nullby callingObject.getPrototypeOftwice. Then call.toString()on it and explain, in one sentence, where that method actually came from. - Create one object with
Object.create(null)and one normal object, then try to call.hasOwnProperty("x")on each. Explain why it works on one and throws on the other, and give one concrete reason a null-prototype object is safer as a dictionary of untrusted keys.
So what did we actually cover?
- The prototype chain is the series of prototype links; property lookup climbs it from the object upward until it finds the property or hits
null. - The lookup algorithm: check own properties, then the prototype, then its prototype, and so on; not found anywhere yields
undefined(not an error), and the closest match always wins. - Every normal object's chain ends at
Object.prototype(source oftoString,hasOwnProperty,valueOf) and thennull. - Built-in types add a link: an array is
arr -> Array.prototype -> Object.prototype -> null, giving it both array methods and generic object methods, and you can even borrow those methods for array-like objects with.call. Object.hasOwnchecks level 0 only; theinoperator checks the whole chain -- so they differ exactly for inherited properties, which is also whyfor...inneeds care.Object.create(null)makes a bare object with no chain: a clean dictionary immune to inherited-key collisions.- Keep chains shallow and set prototypes at creation (never re-parent later) for good performance.
Next episode we stop merely observing the chain and start building with it deliberately -- constructing inheritance the pure JavaScript way, wiring one object to another by hand, before we ever touch a single class keyword.