Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~26 min· topic 3 of 8

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.getPrototypeOf and Object.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.

proto.jswhole filejavascript
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.

proto.jswhole filejavascript
const rabbit = Object.create(animal);
rabbit.name = "Bunny";
rabbit.jumps = true;
Object.getPrototypeOf(rabbit) === animal; // true

03Follow 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.

Follow a lookupdiagram
Rendering diagram…

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.

proto.jswhole filejavascript
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.

Built-in objects use the same chaindiagram
Rendering diagram…

Try it yourself

  1. 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 whether animal.describe changed.

  2. 2

    Make the chain longer

    In the second example, add const baby = Object.create(rabbit); at the end and log baby.eats and baby.sleep(). How many links does the engine follow to find eats?

  3. 3

    Walk your own chain

    In the third example, call chainOf(Object.create(Object.create({}))). Predict the output, then check. Why does every step say Object.prototype or Object?

Code & diagrams

Following the chain New tab

`eats` and `describe` are found by walking up to `animal`. `Object.keys` and `JSON.stringify` only see own properties.

Preview

Press Run to see the page.

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}
Shadowing, and the live link New tab

The link is live: the children don't copy the prototype, they look it up on every read.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

rabbit: false | turtle: true | animal: true
after delete, rabbit: true
zzz zzz
one shared function? true
Built-in chains and a dictionary with no prototype New tab

A 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

Press Run to see the page.

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);.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cyclic __proto__ value

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);.

terminal
$ Console
── what you'll see ──
undefined
undefined

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.setPrototypeOf on 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__ or constructor.prototype can add properties to Object.prototype, which every object then inherits. Defend by rejecting those keys during merges, using Object.create(null) or Map for dictionaries, checking with Object.hasOwn, and, where suitable, freezing Object.prototype in your own app. Test with a unit test that a merge of such keys leaves ({}).polluted undefined.

  • ▸

    Object.prototype is an immutable prototype exotic object: its own [[Prototype]] is always null and can't be changed. Array.prototype is itself an array (Array.isArray(Array.prototype) is true), and Function.prototype is a function, for historical reasons.

  • ▸

    Object.create(proto, descriptors) takes an optional second argument of property descriptors, the same format as Object.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. 1

    Every object has a hidden internal link, written in the spec as **[[Prototype]]**, pointing to another object (or to null). That other object is its prototype. When you read obj.x and obj has no own property (a property stored on the object itself) called x, the engine looks at the prototype, then at its prototype, and so on. This series of links is the prototype chain. If it reaches null without finding x, the result is undefined.

  2. 2

    This is how built-in methods work. [1, 2].map isn't stored on your array: the array's prototype is Array.prototype, which holds map, filter, push and the rest; its prototype is Object.prototype, which holds toString and hasOwnProperty; and its prototype is null, the end. One map function is shared by every array in the program. Methods use this (Topic 7.1) to know which array they were called on.

  3. 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 is proto. 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 {} gets Object.prototype automatically.

  4. 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 on Object.prototype** (see getters in Topic 6.7), kept only for old code, so it doesn't exist on objects that don't inherit from Object.prototype. Use Object.getPrototypeOf and Object.create in new code. (Writing __proto__: proto inside an object literal { } is a separate, standard feature that sets the prototype at creation.)

  5. 5

    Writing is different from reading. obj.x = 5 always creates or changes an own property on obj; it never changes the prototype. If the prototype also has x, the own one now shadows it (hides it) for this object only. delete obj.x removes the own property and the inherited one shows through again. Two exceptions: if the inherited x is a setter, the setter runs; if it is read-only, the write fails (with a TypeError in strict mode).

  6. 6

    To tell own from inherited, use **Object.hasOwn(obj, "x")** (ES2022): true only for own properties. The **in** operator ("x" in obj) checks the whole chain. Object.keys and JSON.stringify see only own properties, while for...in also 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 without toString or any inherited keys.

Explain it without notes

01

What is the prototype chain, and how does property lookup use it?

02

What is the difference between __proto__, Object.getPrototypeOf and a function's prototype property?

03

What happens when you assign a property that already exists on the prototype?

04

Why and when would you use Object.create(null)?

Practice

01

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.

02

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".

03

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.create gives 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.getPrototypeOf and know why __proto__ is legacy.

  • I can tell own from inherited properties with Object.hasOwn and in.

  • I know assignment shadows instead of changing the prototype.

  • I know when to use Object.create(null).