Course outline · 0% complete

0/27 lessons0%

Course overview →

The prototype chain

lesson 3-1 · ~12 min · 6/27

By rule 1, the method call, this is the object before the dot, so this is user.

That rule answers which object a method operates on, and it says nothing about where the method itself came from.

Today covers the other half. Where does JavaScript find hello when it is not on user itself?

The two mechanisms work together on every method call you write. One resolves the function, the other resolves the receiver.

Keeping them separate is what makes the rest of this unit readable, because a method found three links up the chain still gets this from the dot at the call site.

Every object has a hidden link

Every JavaScript object carries a hidden internal link called its prototype, pointing at another object or at null.

When you read a property, the lookup works like the scope chain from lesson 1-1, except it walks objects instead of scopes.

  1. Look on the object itself.
  2. If it is not there, follow the prototype link and look on that object.
  3. Repeat until the property is found, or until the chain ends at null, at which point you get undefined.

That path is the prototype chain.

You can build one directly with Object.create(proto), which makes a new empty object whose prototype is proto.

The reason the language works this way is sharing. A thousand user objects should not carry a thousand copies of the same methods, so JavaScript stores each method once on a shared object and lets lookup find it.

It is also why [1, 2, 3].map(...) works at all. You never wrote map, and the array's prototype supplies it.

One difference from the scope chain is worth noting up front. A missing property yields undefined rather than throwing, while a missing variable is a ReferenceError.

Finding a method one link up

rex has no describe of its own.

const animal = {
  describe() {
    return this.name + " the " + this.kind;
  },
};

const rex = Object.create(animal);
rex.name = "Rex";
rex.kind = "dog";

console.log(rex.describe());
console.log(Object.getPrototypeOf(rex) === animal);
console.log(rex.hasOwnProperty("describe"));

Output

Rex the dog
true
false

The lookup follows the prototype link to animal and finds describe there.

this is still rex, because of how the method was called. The function came from animal, and the dot in rex.describe() supplied the receiver, which is why it reads rex's name and kind.

That split is the whole point. animal has no name at all, so a method that used animal.name instead of this.name would be useless for sharing.

hasOwnProperty returning false is the precise statement of what "own" means. The property is readable and it is not stored on this object.

hasOwnProperty itself is a good illustration of the mechanism, since rex did not define it and neither did animal. It was found further along the chain.

rexname, kindanimaldescribe()Object.prototypehasOwnProperty()looking for describe…lookup walks left to right until it finds the property or hits null
Property lookup walks the prototype chain: rex, then animal, then Object.prototype, then null.

It works. The lookup continues to Object.prototype, which has toString.

Almost every object's chain ends at Object.prototype, and that object supplies toString, hasOwnProperty, valueOf, and a few others.

That is why plain objects have methods you never wrote, and why String(rex) produces something instead of failing.

For this object the answer is the unhelpful "[object Object]", which is the default implementation. Defining toString on animal would shadow it for every animal at once.

Only a chain that ends earlier would throw. Object.create(null) makes an object with no prototype at all, so rex.toString() raises a TypeError.

Prototype-free objects are occasionally the right tool. They make safe dictionaries, since no user-supplied key can collide with an inherited name like constructor.

Writes do not climb the chain

The chain is for reads only.

Assigning cat.sound = "meow" never modifies the prototype. It creates an own property on cat that shadows the inherited one, which is the same word and the same idea as variable shadowing in lesson 1-1.

Delete the own property and the inherited value shows through again, because the read resumes its climb.

This asymmetry is deliberate. One instance changing a value must not silently change its siblings, and a write that climbed the chain would do exactly that.

The flip side is the part that bites. Mutating the shared prototype object itself does affect every instance at once, which is a classic source of spooky bugs.

The most common real version of this involves arrays and objects. A prototype holding tags: [] gives every instance the same array, and one instance pushing to it changes them all, because pushing is a mutation rather than an assignment.

Shadowing an inherited value

One read, one write, one delete, one read again.

const animal = { sound: "generic" };
const cat = Object.create(animal);

console.log(cat.sound);
cat.sound = "meow";
console.log(cat.sound);
console.log(animal.sound);
delete cat.sound;
console.log(cat.sound);

Output

generic
meow
generic
generic

The first read climbs to animal. The write creates cat's own sound, shadowing the inherited one.

The third line proves animal was never touched, which is the guarantee that keeps siblings independent.

Deleting the own property reveals the prototype's value again, so the final read prints generic.

delete is doing something subtler than it looks. It removes the own property and cannot remove an inherited one, so a second delete cat.sound changes nothing.

cat.hasOwnProperty("sound") traces the whole story if you run it after each step, returning false, then true, then false again.

A shape prototype

One method shared by any object with a width and a height.

const shape = {
  area() {
    return this.width * this.height;
  },
};

const box = Object.create(shape);
box.width = 4;
box.height = 5;

console.log(box.area());

Output

20

shape is a plain object literal holding one method, and Object.create(shape) makes an object whose prototype is shape.

box.area() finds area on shape, and this is box, so it reads box's own width and height.

shape has no width or height, and it does not need them. It contributes behavior, and each instance contributes data.

Creating a second box the same way shares the one area function rather than copying it, which is the memory argument for prototypes in a sentence.

Adding a perimeter method to shape after box exists still works on box immediately, because the chain is a live link rather than a snapshot taken at creation time.