Topic 7.3
Prototypes and the Prototype Chain
In one line
Every object has a hidden link to another object, its prototype; when you read a property the object doesn't have, JavaScript follows that link, and the next one, up the prototype chain until it finds the property or reaches null.
Think of it like this
Asking for a pencil in class. You look in your own bag first. If it's not there, you ask your desk partner. If they don't have one, they ask the teacher. The question goes up the line until someone has a pencil, or the line ends and the answer is "nobody has one". A JavaScript object does the same when you ask for a property: it checks itself, then its prototype, then the prototype's prototype, until the chain ends.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Prototype
- The object another object is linked to and borrows properties from.
- [[Prototype]]
- The spec's name for the hidden link from an object to its prototype.
- Prototype chain
- The line of links from an object to its prototype, to that one's prototype, and so on until
null. - Own property
- A property stored directly on the object itself, not borrowed from the chain.
- Inherited property
- A property the object doesn't have itself but finds on its prototype chain.
- Shadowing
- An own property with the same name as an inherited one, hiding the inherited one for that object.
- Object.create
- Makes a new object and sets its prototype to the object you pass in.
- __proto__
- An old getter/setter for the prototype link. Prefer
Object.getPrototypeOfandObject.create. - Null-prototype object
- An object made with
Object.create(null): it has no prototype, so it inherits nothing.
Step by step
01Make an object to borrow from
animal holds what every animal shares: a property and a method that uses this.
const animal = {
eats: true,
describe() {
return this.name + " eats: " + this.eats;
},
};02Link a new object to it
Object.create(animal) makes an empty object whose prototype is animal. Then we give it two own properties.
const rabbit = Object.create(animal);
rabbit.name = "Bunny";
rabbit.jumps = true;
Object.getPrototypeOf(rabbit) === animal; // true03Follow a lookup
rabbit.jumps is found on rabbit. rabbit.eats isn't, so the engine checks animal and finds it. rabbit.toString is on neither, so it continues to Object.prototype. rabbit.flies is nowhere, so the chain ends at null and the answer is undefined.
rabbit.describe() finds describe on animal, but calls it with this = rabbit (the object before the dot), so it reads Bunny's name.
04Write creates an own property
rabbit.eats = false doesn't touch animal. It adds an own eats to rabbit that shadows the inherited one. Other objects linked to animal still see true.
rabbit.eats = false;
rabbit.eats; // false (own)
animal.eats; // true (unchanged)
delete rabbit.eats;
rabbit.eats; // true again (inherited)05Built-in objects use the same chain
An array literal is linked to Array.prototype, a function to Function.prototype, and both of those to Object.prototype. That's why every array has map and every object has toString.
Try it yourself
- 1
Shadow a method
In the first example, add
rabbit.describe = function () { return "I am a rabbit"; };before the logs. Predict the second line, and whetheranimal.describechanged. - 2
Make the chain longer
In the second example, add
const baby = Object.create(rabbit);at the end and logbaby.eatsandbaby.sleep(). How many links does the engine follow to findeats? - 3
Walk your own chain
In the third example, call
chainOf(Object.create(Object.create({}))). Predict the output, then check. Why does every step sayObject.prototypeorObject?
Code & diagrams
`eats` and `describe` are found by walking up to `animal`. `Object.keys` and `JSON.stringify` only see own properties.
Preview
Console
Nothing logged yet.Expected console output
true true undefined
Bunny eats: true
prototype is animal? true
own eats? false | eats in rabbit? true
own keys: [ 'name', 'jumps' ]
{"name":"Bunny","jumps":true}The link is live: the children don't copy the prototype, they look it up on every read.
Preview
Console
Nothing logged yet.Expected console output
rabbit: false | turtle: true | animal: true
after delete, rabbit: true
zzz zzz
one shared function? trueA null-prototype object inherits nothing, so a key like `toString` or `__proto__` can't collide with a built-in. `Map` is usually even better for dictionaries.
Preview
Console
Nothing logged yet.Expected console output
[1, 2]: Array.prototype -> Object.prototype -> null
function: Function.prototype -> Object.prototype -> null
new Map(): Map.prototype -> Object.prototype -> null
{}: Object.prototype -> null
false true
[Object: null prototype] { apple: 3 }Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Create a cycle in the chain
Write const a = {}; const b = Object.create(a); Object.setPrototypeOf(a, b);.
Break #2
Use __proto__ on a null-prototype object
Write const d = Object.create(null); console.log(d.__proto__); d.__proto__ = { x: 1 }; console.log(d.x);.
Myth vs fact
Myth
Objects copy the properties of their prototype.
Fact
Nothing is copied. The object keeps a link and looks properties up on every read, which is why adding to a prototype later affects existing objects.
Myth
obj.prototype is the object's prototype.
Fact
No. A plain object has no prototype property at all. Its prototype is the hidden [[Prototype]], read with Object.getPrototypeOf(obj). The property called prototype exists on functions, and it becomes the prototype of objects made with new (Topic 7.4).
Myth
Setting a property on an object can change its prototype's property.
Fact
Assignment creates or updates an own property and shadows the inherited one. Only an inherited setter can intercept it, and only a read-only inherited property can block it.
Myth
JavaScript has no inheritance because it has no classes.
Fact
It has prototypal inheritance: objects inherit from objects. class (Topic 7.5) is a nicer syntax built on that same mechanism.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Engines give objects hidden shapes (V8 calls them maps or hidden classes) and cache property lookups at each access site (inline caches). Lookups up a stable chain are fast.
Object.setPrototypeOfon an existing object, or changing prototypes that many objects use, invalidates those caches; MDN warns that it is very slow in every engine. Create objects with the right prototype from the start. - ▸
Prototype pollution is a security bug class: code that merges untrusted JSON into objects with a key like
__proto__orconstructor.prototypecan add properties toObject.prototype, which every object then inherits. Defend by rejecting those keys during merges, usingObject.create(null)orMapfor dictionaries, checking withObject.hasOwn, and, where suitable, freezingObject.prototypein your own app. Test with a unit test that a merge of such keys leaves({}).pollutedundefined. - ▸
Object.prototypeis an immutable prototype exotic object: its own[[Prototype]]is alwaysnulland can't be changed.Array.prototypeis itself an array (Array.isArray(Array.prototype)istrue), andFunction.prototypeis a function, for historical reasons. - ▸
Object.create(proto, descriptors)takes an optional second argument of property descriptors, the same format asObject.defineProperties(Topic 6.7), so you can create an object with its prototype and non-enumerable or read-only properties in one step.
Remember this
- 1
Every object has a hidden internal link, written in the spec as **
[[Prototype]]**, pointing to another object (or tonull). That other object is its prototype. When you readobj.xandobjhas no own property (a property stored on the object itself) calledx, the engine looks at the prototype, then at its prototype, and so on. This series of links is the prototype chain. If it reachesnullwithout findingx, the result isundefined. - 2
This is how built-in methods work.
[1, 2].mapisn't stored on your array: the array's prototype isArray.prototype, which holdsmap,filter,pushand the rest; its prototype isObject.prototype, which holdstoStringandhasOwnProperty; and its prototype isnull, the end. Onemapfunction is shared by every array in the program. Methods usethis(Topic 7.1) to know which array they were called on. - 3
Reading the link:
Object.getPrototypeOf(obj)returns the prototype. Creating an object with a chosen prototype:Object.create(proto)makes a new empty object whose prototype isproto. Changing the link later:Object.setPrototypeOf(obj, proto)works, but engines optimise objects by their shape and chain, so changing it after creation makes code slower; set it at creation instead. A plain{}getsObject.prototypeautomatically. - 4
**
__proto__** (two underscores on each side) is an old way to read and set the same link:obj.__proto__. It is a **getter/setter onObject.prototype** (see getters in Topic 6.7), kept only for old code, so it doesn't exist on objects that don't inherit fromObject.prototype. UseObject.getPrototypeOfandObject.createin new code. (Writing__proto__: protoinside an object literal{ }is a separate, standard feature that sets the prototype at creation.) - 5
Writing is different from reading.
obj.x = 5always creates or changes an own property onobj; it never changes the prototype. If the prototype also hasx, the own one now shadows it (hides it) for this object only.delete obj.xremoves the own property and the inherited one shows through again. Two exceptions: if the inheritedxis a setter, the setter runs; if it is read-only, the write fails (with a TypeError in strict mode). - 6
To tell own from inherited, use **
Object.hasOwn(obj, "x")** (ES2022):trueonly for own properties. The **in** operator ("x" in obj) checks the whole chain.Object.keysandJSON.stringifysee only own properties, whilefor...inalso walks inherited enumerable properties (Topic 2.7). The prototype is live: add a method to a prototype later and every object linked to it can use it at once.Object.create(null)makes an object with no prototype, a clean dictionary withouttoStringor any inherited keys.
Explain it without notes
What is the prototype chain, and how does property lookup use it?
What is the difference between __proto__, Object.getPrototypeOf and a function's prototype property?
What happens when you assign a property that already exists on the prototype?
Why and when would you use Object.create(null)?
Practice
Create const vehicle = { wheels: 4, info() { return this.type + " with " + this.wheels + " wheels"; } }. Make bike from it with Object.create, give it type: "bike" and its own wheels: 2, and print bike.info() and vehicle.wheels.
Write a function isInherited(obj, key) that returns true only when key can be read on obj but isn't an own property. Test it with [] and "map", and with { a: 1 } and "a".
Write a function depth(obj) that returns how many links there are from obj to null. What does it return for {}, [] and Object.create(null)?
Trade-offs
- ↔
Sharing methods through a prototype saves memory (one function for thousands of objects), but a long chain makes lookups of missing properties walk further and makes it harder to see where a property comes from.
- ↔
Object.creategives full control of the chain with plain objects, while constructors and classes (next topics) are more familiar to most readers and tools.
Done when you can
I can explain how a property read walks the prototype chain to
null.I can create an object with a chosen prototype using
Object.create.I can read a prototype with
Object.getPrototypeOfand know why__proto__is legacy.I can tell own from inherited properties with
Object.hasOwnandin.I know assignment shadows instead of changing the prototype.
I know when to use
Object.create(null).