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
| Operation | Use | Notes |
|---|---|---|
Object.create(proto) | make a new object with a chosen prototype | The clearest way to build a chain by hand. |
Object.create(null) | make an object with no prototype at all | A safe dictionary: no inherited keys, and no toString. |
Object.getPrototypeOf(obj) | read the link | The standard reader. Works on any object. |
Object.setPrototypeOf(obj, p) | change the link after creation | Legal, slow, almost always a design smell. |
obj.__proto__ | read or write the link | A legacy accessor, kept for the web. Prefer the two functions above. |
Fn.prototype | the object new Fn() instances will link to | A 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
- Reads walk up The first object in the chain that owns the key wins. The search stops there, so nearer definitions hide further ones.
- Writes stay put Assignment creates an own property on the object you assigned to. The prototype is never touched. That is shadowing.
thisis the receiver, not the owner An inherited method still runs withthisset 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); // trueA 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.
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] —
rexwas created bynew Dog(), so its link points atDog.prototype.Dogitself is a function object, so its link points atFunction.prototype, not at its own.prototypeproperty. 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 andb.list.lengthis 1.b.name = "b"is an assignment, which creates an own property onband leavesproto.namealone. - 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.createornewinstead. - Which check tells you whether a key belongs to the object itself rather than something up the chain?
Object.hasOwn(obj, key)—insearches the whole chain, so"toString" in {}is true. Comparing againstundefinedcannot tell a missing key from a key set toundefined.Object.hasOwnasks exactly the own-property question, and unlikeobj.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 objto validate user data and matching inherited keys liketoStringorconstructor. - Reaching for
Object.setPrototypeOfwhenObject.createat construction time would do the same job for free. - Expecting
Object.keysto show inherited keys. It shows own enumerable keys only.
Takeaways
- Every object has a
[[Prototype]]link that is either another object ornull. - 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
thisset to the object you called it on. __proto__points up from an object;.prototypepoints down at a constructor’s future instances.- Fix the prototype at construction time with
Object.createornew, not afterwards withsetPrototypeOf.