Prototypes

Mental model: Every object holds a hidden link to another object. A failed property lookup follows that link, over and over, until it reaches null.

Level: intermediate · about 16 minutes

const point = { x: 1, y: 2 };

console.log(Object.hasOwn(point, 'toString'));  // false
console.log(typeof point.toString);             // 'function'
console.log(point.toString());                  // '[object Object]'

You never wrote toString, yet it is there.

The property is not on point. It is on another object that point is linked to, and the lookup found it there. That link is the whole subject of this lesson.

Every object has an internal slot the spec writes as [[Prototype]]. It holds either another object or null. When you read obj.key and obj has no own property key, the engine follows [[Prototype]] and asks that object the same question, then its prototype, and so on. The sequence of objects it walks is the prototype chain.

Reading and setting the link

const base = { greet() { return 'hi from base'; } };
const child = Object.create(base);          // child's [[Prototype]] is base

console.log(child.greet());                        // 'hi from base'
console.log(Object.getPrototypeOf(child) === base); // true
console.log(Object.keys(child));                   // [] - it owns nothing

base.extra = 'added later';
console.log(child.extra);                          // 'added later' - live link, not a copy
OperationUseNotes
Object.create(proto)make a new object with a chosen prototypeThe clearest way to build a chain by hand.
Object.create(null)make an object with no prototype at allA safe dictionary: no inherited keys, and no toString.
Object.getPrototypeOf(obj)read the linkThe standard reader. Works on any object.
Object.setPrototypeOf(obj, p)change the link after creationLegal, slow, almost always a design smell.
obj.__proto__read or write the linkA legacy accessor, kept for the web. Prefer the two functions above.
Fn.prototypethe object new Fn() instances will link toA plain property on a function, not the function’s own prototype.

obj.__proto__: my own link

const dog = { bark: () => 'woof' };
const pup = Object.create(dog);

Object.getPrototypeOf(pup) === dog;
// true: pup looks up into dog

// A function object's own link:
function f() {}
Object.getPrototypeOf(f) === Function.prototype;
// true

Fn.prototype: my instances’ link

function Dog() {}
Dog.prototype.bark = () => 'woof';

const rex = new Dog();
Object.getPrototypeOf(rex) === Dog.prototype;
// true

Dog.prototype === Dog.__proto__;
// false. Different objects entirely.

Say it out loud once: "dunder proto points up, dot prototype points down at instances." Only functions that can be constructed have a useful .prototype; arrow functions and methods do not have one at all.

function classic() {}
const arrow = () => {};
const shorthand = { m() {} }.m;

console.log(typeof classic.prototype);    // 'object'
console.log(typeof arrow.prototype);      // 'undefined'
console.log(typeof shorthand.prototype);  // 'undefined'

Proof that arrows are not constructors.

The chain, drawn

  const nums = [1, 2, 3]        const plain = { a: 1 }
          |                             |
          v                             v
   Array.prototype                Object.prototype
   (map, filter, join)            (toString, hasOwnProperty,
          |                        valueOf, isPrototypeOf)
          v                             |
   Object.prototype  <------------------+
          |
          v
        null            <- the chain always terminates here
function chain(value) {
  const out = [];
  let current = Object.getPrototypeOf(value);
  while (current !== null) {
    out.push(current.constructor?.name ?? '(anonymous)');
    current = Object.getPrototypeOf(current);
  }
  return [...out, 'null'];
}

console.log(chain([1, 2]));        // ['Array', 'Object', 'null']
console.log(chain('hi'));          // ['String', 'Object', 'null']
console.log(chain(() => {}));      // ['Function', 'Object', 'null']
console.log(chain(Object.create(null)).length); // 1: just 'null'

Walk any chain yourself with a five-line loop.

Lookup and shadowing

  1. Reads walk up The first object in the chain that owns the key wins. The search stops there, so nearer definitions hide further ones.
  2. Writes stay put Assignment creates an own property on the object you assigned to. The prototype is never touched. That is shadowing.
  3. this is the receiver, not the owner An inherited method still runs with this set to the object you called it on. That is what makes a single shared method useful to a thousand instances.
const proto = { tags: [] };
const one = Object.create(proto);
const two = Object.create(proto);

one.tags.push('shared');            // mutation, not assignment
console.log(two.tags);              // ['shared'] - surprise

two.tags = ['own'];                 // assignment shadows
console.log(proto.tags, two.tags);  // ['shared'] ['own']
const base = { value: 1 };
const mid = Object.create(base);
const leaf = Object.create(mid);

mid.value = 2;
console.log(leaf.value);
delete mid.value;
console.log(leaf.value);

Assigning to mid gives mid an own value of 2, which shadows base for anything below it, so leaf.value reads 2. Deleting the shadow lets the lookup continue past mid to base, so it reads 1 again. Nothing was ever copied into leaf.

What Object.prototype gives you

`hasOwnProperty(k)`
own property only, ignores the chain. Object.hasOwn(obj, k) is the modern spelling
`isPrototypeOf(obj)`
is this object anywhere in obj’s chain
`toString()`
the default [object Object], and the borrowable type tag
`valueOf()`
the hook coercion calls when it wants a primitive
`propertyIsEnumerable(k)`
own and enumerable, so it survives spread and Object.keys
`constructor`
not a method: a back reference to the function that owns this prototype
const dict = Object.create(null);
dict.constructor = 'just a string';    // no inherited meaning to break
console.log('toString' in dict);       // false
console.log(Object.getPrototypeOf(dict)); // null

const risky = {};
console.log('toString' in risky);      // true - inherited keys leak into `in`
console.log(risky.constructor === Object); // true

A prototype-free object is the only truly safe key/value bag.

Why setPrototypeOf is slow

Engines optimise property access by giving every object a hidden class (V8 calls it a shape or a map) that records its layout, including its prototype. Call sites are then compiled against that shape, and repeated lookups become a couple of machine instructions. Changing an object’s prototype after creation invalidates the shape, throws away the inline caches that depended on it, and can push the object into a slower dictionary mode for the rest of its life.

Slow: mutate the link

const obj = { a: 1 };
Object.setPrototypeOf(obj, proto);
// shape invalidated, caches dropped,
// every call site that touched obj
// has to be re-learned

Fast: choose the link up front

const obj = Object.create(proto);
obj.a = 1;

// or, when you already have the data:
const obj2 = Object.assign(
  Object.create(proto),
  { a: 1 },
);

Same end state, very different cost. Treat [[Prototype]] as decided at construction time. The MDN docs on setPrototypeOf carry an explicit performance warning, which is unusual for a standard method.

There are no classes at the bottom. There are objects that delegate to other objects.

The prototype chain, summarised

Interactive visualiser: proto. Enable JavaScript to use it.

Try it yourself

Walk a chain

const chainOf = (v) => {
  const links = [];
  for (let p = Object.getPrototypeOf(v); p !== null; p = Object.getPrototypeOf(p)) {
    links.push(p.constructor?.name ?? '(none)');
  }
  return links.join(' -> ') + ' -> null';
};

console.log(chainOf([]));
console.log(chainOf({}));
console.log(chainOf(new Date(0)));

Add a case for new Map(), for a class instance, and for Object.create(null). Which one has the longest chain?

Shadow and unshadow

const proto = { label: 'proto', items: [] };
const a = Object.create(proto);
const b = Object.create(proto);

a.label = 'a-own';
console.log(a.label, b.label, proto.label);
console.log(Object.hasOwn(a, 'label'), Object.hasOwn(b, 'label'));

delete a.label;
console.log(a.label);

a.items.push(1);
console.log(b.items);

Shadow label, read it, delete the shadow, read it again. Then try mutating proto.items from two children.

Exercises

List a prototype chain

Write protoChain(value) that returns an array of the objects in the value’s prototype chain, nearest first, stopping before null. protoChain([]) gives [Array.prototype, Object.prototype]. An object made with Object.create(null) has an empty chain. Do not use __proto__.

Defaults that stay live

Write withDefaults(defaults). It returns a factory make(overrides) that produces a config object whose prototype is the defaults object. Overrides become own properties. Reading a key nobody overrode falls through to defaults, so editing defaults later shows up in every config that has not shadowed that key. Never copy the defaults in.

Check yourself

What does this log?
[true, false] — rex was created by new Dog(), so its link points at Dog.prototype. Dog itself is a function object, so its link points at Function.prototype, not at its own .prototype property. A constructor is not an instance of itself.
What does this print?
1 "proto" — a.list.push() is a read followed by a mutation, so both objects share the one array on the prototype and b.list.length is 1. b.name = "b" is an assignment, which creates an own property on b and leaves proto.name alone.
Why is Object.setPrototypeOf(obj, proto) discouraged in hot code?
It invalidates the object’s hidden class and the inline caches compiled against it, so later property access gets much slower — It is standard and it works. The cost is that engines specialise property access on an object’s shape, and the prototype is part of that shape. Changing it throws that work away. Choose the prototype at creation time with Object.create or new instead.
Which check tells you whether a key belongs to the object itself rather than something up the chain?
Object.hasOwn(obj, key) — in searches the whole chain, so "toString" in {} is true. Comparing against undefined cannot tell a missing key from a key set to undefined. Object.hasOwn asks exactly the own-property question, and unlike obj.hasOwnProperty(key) it still works when the object has no prototype.

Common mistakes

  • Believing inherited properties are copied into the instance. There is one shared object and a live link.
  • Mixing up __proto__ (an object’s own link, pointing up) with .prototype (a constructor’s blueprint for its instances).
  • Putting an array or object on a prototype, then mutating it and hitting cross-instance bleed.
  • Using key in obj to validate user data and matching inherited keys like toString or constructor.
  • Reaching for Object.setPrototypeOf when Object.create at construction time would do the same job for free.
  • Expecting Object.keys to show inherited keys. It shows own enumerable keys only.

Takeaways

  • Every object has a [[Prototype]] link that is either another object or null.
  • A failed lookup walks the chain and stops at the first object that owns the key.
  • Reads walk up the chain; writes always create an own property, which shadows.
  • An inherited method still runs with this set to the object you called it on.
  • __proto__ points up from an object; .prototype points down at a constructor’s future instances.
  • Fix the prototype at construction time with Object.create or new, not afterwards with setPrototypeOf.