Every object in JavaScript carries a hidden reference to another object, and property lookup follows that chain until it finds what it wants or runs out.
Complete lesson & earn 250 PX
EXERCISE
1You have been using the prototype chain since your first line of JavaScript, every time you called a method you did not write.
EXERCISE
2You can set the link yourself, and watching lookup succeed and fail across it is the fastest way to make the mechanism concrete.
EXERCISE
3Every chain terminates at null, and the object just before that terminus is where the methods common to every object in your program live.
Start with a question you have never had a reason to ask. You write an array, you call .map on it, and it works. Where did .map come from? You did not write it. It is not in your file. It is not something you imported.
The usual answer is "it is built in", and that answer is wrong in an interesting way. Nothing was built into your array. What actually happened is that JavaScript looked for .map on your array, did not find it, and then went looking somewhere else. That somewhere else is what this unit is about.
First, confirm the method is genuinely not on the object:
const nums = [1, 2, 3];
console.log(Object.keys(nums)); // [ '0', '1', '2' ]
console.log(nums.hasOwnProperty("map")); // false
console.log(typeof nums.map); // "function"
Read those three lines carefully, because they contradict each other on the surface. The array's own properties are the three indexes and nothing else. Asked directly whether it owns a property called map, it says no. Asked for nums.map, it hands you a function. Both answers are true, and the gap between them is the mechanism.
Every object carries a hidden link to another object:
const user = { name: "Ada" };
console.log(Object.getPrototypeOf(user) === Object.prototype); // true
console.log(user.__proto__ === Object.prototype); // true, older syntax
console.log(user.toString()); // "[object Object]"
That link has a formal name in the specification, [[Prototype]], written in double brackets to mark it as internal. You read it with Object.getPrototypeOf. You will also see it written __proto__, which is older, still works everywhere, and is kept alive purely because too much code depends on it.
When a property is not found on an object, JavaScript follows that link and asks the next object. If it is not there either, it follows that object's link, and so on. The sequence of objects it visits is the prototype chain, and the search stops the moment a name matches.
Watch the search across several steps:
const nums = [1, 2, 3];
// nums -> Array.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
console.log(nums.definitelyNotAMethod); // undefined, not an error
.map was found at the first step, on Array.prototype. .toString would have been found at the second, on Object.prototype. A name that exists nowhere returns undefined, because the chain ran out rather than because anything went wrong.
This is worth slowing down on, because it changes what "a object" means. An object is not a sealed holding everything it can do. It is a small bag of its own properties plus a pointer to somewhere else to look. Almost everything an object appears to know is borrowed from further up.
There is no hidden list of built-in methods. map, filter, toString and every other method you did not write are ordinary properties sitting on ordinary objects, one or two links up. This matters beyond trivia: it is why adding a method to Array.prototype instantly gives every array in your program that method, why a library can extend built-in types, and why an accidental property on a prototype can silently affect objects written years apart.
Reading about a chain is one thing. Building one and then watching a property get found three objects away is what makes it stick. Object.setPrototypeOf takes two objects and makes the first one point at the second.
A warning before you use it: this is a teaching tool, not a production one. Changing an object's prototype after the object exists forces JavaScript engines to throw away the optimisations they had built for it, and the cost is large enough that the practice is discouraged everywhere. You will meet the correct way to do this in lesson 3. For now it is the clearest window into the machinery.
Two plain objects, linked, and one property found across the link:
const animal = {
breathe() {
return `${this.name} breathes`;
},
};
const dog = { name: "Rex" };
console.log(dog.breathe); // undefined -- nowhere to be found
Object.setPrototypeOf(dog, animal);
console.log(dog.breathe()); // Rex breathes
console.log(dog.hasOwnProperty("breathe")); // false
Look closely at what breathe returned. The function is defined on animal, and animal has no name property at all. Yet this.name produced "Rex". That is not a coincidence and it is not a special case for prototypes.
The rule is: this is the object the method was called on, not the object the method was found on. The search for breathe went up the chain; the value of this stayed with dog, because dog is what sits to the left of the dot. That one sentence is why shared methods on a prototype are useful at all, and it is the foundation of the whole of Unit 2.
Three levels, to show the search does not stop at one:
const living = { alive: true };
const animal = { legs: 4 };
const dog = { name: "Rex" };
Object.setPrototypeOf(animal, living);
Object.setPrototypeOf(dog, animal);
console.log(dog.name); // Rex found on dog itself
console.log(dog.legs); // 4 found one level up
console.log(dog.alive); // true found two levels up
console.log(dog.wings); // undefined chain exhausted
Now the part that catches people. Reading and writing are not symmetric:
console.log(dog.legs); // 4, inherited from animal
dog.legs = 3; // this writes to DOG, never to animal
console.log(dog.legs); // 3
console.log(animal.legs); // 4 <- untouched
console.log(dog.hasOwnProperty("legs")); // true, now it owns one
A read climbs the chain until it finds something. A write does not climb at all. An assignment always creates or updates a property on the object you assigned to, and if a property of that name existed further up, the new own property simply hides it from then on. That hiding is called shadowing.
Most of the time this is exactly what you want, and it is why one shared prototype can serve thousands of objects that each override a value or two. It becomes a bug the moment you assume otherwise. obj.count++ on an inherited number reads the prototype's value, adds one, and writes the result as a brand new own property.
Pro Tip: Reads climb, writes land. Hold those three words and a whole family of confusing behaviour becomes predictable: why changing one object never affects its siblings, why a shared default silently stops being shared the first time someone assigns to it, and why counters and accumulators on a prototype almost never do what their author intended.
A chain that never ended would be a program that hangs on the first typo. So every chain stops, and it stops in the same place: an object called Object.prototype, whose own link is null.
You can walk a chain yourself and watch it end. This is the kind of loop worth typing into a console once, because the output makes the shape concrete in a way a diagram does not.
Walking a chain to its terminus:
const user = { name: "Ada" };
let current = user;
while (current !== null) {
console.log(current === Object.prototype ? "Object.prototype" : current);
current = Object.getPrototypeOf(current);
}
// { name: 'Ada' }
// Object.prototype
// then null, and the loop ends
Two objects, then nothing. That is the entire chain of a plain object literal.
Which explains a fact you have probably taken for granted:
console.log(Object.getOwnPropertyNames(Object.prototype));
// [ 'constructor', 'hasOwnProperty', 'isPrototypeOf',
// 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf', ... ]
Every object in your program can be converted to a string, compared, and asked what it owns. Not because those abilities were stamped into each object when it was created, but because every chain, from an array to a date to a function to something you wrote yourself, passes through this one object on its way to null. Roughly a dozen methods, defined once, reachable from everything.
An object with no chain at all makes the point unarguable:
const bare = Object.create(null);
bare.name = "Ada";
console.log(bare.name); // Ada
console.log(bare.toString); // undefined
// bare.toString(); // TypeError: bare.toString is not a function
console.log(Object.getPrototypeOf(bare)); // null
It stores and returns its own property perfectly well. It simply cannot do any of the things you thought objects could do, because those things were never its own. Object.create gets a full treatment in lesson 3; it is here because a chainless object is the clearest possible proof that the common methods are inherited, not built in.
And there is one place where wanting no chain is the correct engineering decision:
// A plain object used as a dictionary answers to names nobody added:
const lookup = {};
console.log(lookup["toString"]); // a function, not undefined
console.log("constructor" in lookup); // true
// So a user-supplied key can collide with an inherited name:
const counts = {};
const word = "constructor";
console.log(counts[word]); // a function, not undefined
// counts[word] = (counts[word] || 0) + 1; // now you are adding 1 to a function
// A chainless object has nothing to collide with:
const safe = Object.create(null);
console.log(safe["toString"]); // undefined
console.log("constructor" in safe); // false
That word-counting bug is a real one that ships regularly, and it is unusually hard to find because it only appears when a user happens to type one of about a dozen specific words. If you are building a lookup table whose keys come from outside your program, a chainless object removes the entire category.
The chain ending at null is what makes a failed lookup return undefined rather than loop forever, and the object sitting one step before null is why every object already knows how to describe itself. Nothing here was built in. It was all inherited, from one object that every chain passes through, and you can opt out of that inheritance when it gets in your way.
Each object has an internal [[Prototype]] link. A failed property lookup walks that link upward, one object at a time, ending at Object.prototype and then null. That chain is where every built-in method you have ever called actually lives.