Emmanuel Akinfulubi

Lesson 06 of 1516 min read

Prototypes and the Chain Above

Every object in JavaScript hangs from another object. When you ask for something it does not have, the search walks up that chain. This one mechanism is all of JavaScript inheritance.

By the end: Trace a property lookup all the way up the chain.

Every bus in the park belongs to a company. Ask the bus something it cannot answer and you ask the company. Ask the company something it does not know and you ask the park office. Above the park office there is nobody left to ask.

Objects in JavaScript work the same way, and this is the only way the language does inheritance. Classes, extends, super: all of it is this.

A lookup that walks upward

const parkOffice = {
  park: 'Jibowu park',
};

// Object.create makes a new object whose prototype is the one you pass in.
const company = Object.create(parkOffice);
company.kind = 'luxury coach';

const bus = Object.create(company);
bus.plate = 'LAG-441-XA';

console.log(bus.plate);      // found on the object itself
console.log(bus.kind);       // not there: found one level up
console.log(bus.park);       // not there either: found two levels up
console.log(bus.nonsense);   // never found

// The object genuinely owns only one property.
console.log('own keys:', Object.keys(bus));
console.log('owns kind?', Object.hasOwn(bus, 'kind'));
console.log('can reach kind?', 'kind' in bus);

Output


      

Seeing the chain

Walk the chain yourself

function Bus(plate) { this.plate = plate; }
Bus.prototype.describe = function () { return 'I am ' + this.plate; };

const b = new Bus('LAG-441-XA');

let link = b;
let step = 0;
while (link !== null) {
  const label = step === 0
    ? 'the object itself'
    : (link.constructor ? link.constructor.name + '.prototype' : 'an object');
  console.log(step + ': ' + label, '| own keys:', Object.getOwnPropertyNames(link).join(', '));
  link = Object.getPrototypeOf(link);
  step++;
}
console.log(step + ': null — end of the chain');

Output


      

So b.describe() is not found on b. It is found on Bus.prototype, one step up. And b.toString() is found on Object.prototype, two steps up, which is why every object in JavaScript can be turned into text without you writing anything.

Where common methods actually live

const plain = {};

console.log('does {} own toString?', Object.hasOwn(plain, 'toString'));
console.log('can {} use toString?', typeof plain.toString);
console.log('where does it live?', Object.hasOwn(Object.prototype, 'toString'));

const list = [1, 2, 3];
console.log('array owns map?', Object.hasOwn(list, 'map'));
console.log('Array.prototype owns map?', Object.hasOwn(Array.prototype, 'map'));
console.log('array chain:',
  Object.getPrototypeOf(list) === Array.prototype,
  Object.getPrototypeOf(Array.prototype) === Object.prototype);

Output


      

The three names for nearly the same thing

This is where most confusion lives, and it is worth being precise for two minutes.

The three, side by side

function Bus(plate) { this.plate = plate; }
const b = new Bus('LAG-441-XA');

console.log('instance hangs from Bus.prototype:',
  Object.getPrototypeOf(b) === Bus.prototype);

console.log('__proto__ is the same thing:',
  b.__proto__ === Bus.prototype);

// The constructor function is a function, so IT hangs from Function.prototype.
console.log('Bus itself hangs from Function.prototype:',
  Object.getPrototypeOf(Bus) === Function.prototype);

// And prototype objects have a constructor property pointing back.
console.log('round trip:', Bus.prototype.constructor === Bus);

Output


      

Shadowing: the object always wins

If an object has its own copy of a property, the chain is never consulted. The lookup stops at the first match.

An own property shadows the inherited one

function Bus(plate) { this.plate = plate; }
Bus.prototype.slogan = 'Safe Journey';

const a = new Bus('LAG-441-XA');
const b = new Bus('ABJ-772-KM');

console.log(a.slogan, '/', b.slogan);

// Assignment always writes to the object itself, never up the chain.
a.slogan = 'No Food For Lazy Man';

console.log(a.slogan, '/', b.slogan);
console.log('a owns it now:', Object.hasOwn(a, 'slogan'));
console.log('b still borrows it:', Object.hasOwn(b, 'slogan'));

// Delete the own copy and the inherited one reappears.
delete a.slogan;
console.log('after delete:', a.slogan);

Output


      

The shared-array trap, live

function Bus(plate) { this.plate = plate; }
Bus.prototype.faults = [];       // one array, shared by everything

const a = new Bus('LAG-441-XA');
const b = new Bus('ABJ-772-KM');

a.faults.push('cracked windscreen');

console.log('a.faults:', a.faults);
console.log('b.faults:', b.faults);   // not empty. Same array.
console.log('same array?', a.faults === b.faults);

Output


      

The fix: per-instance state belongs on the instance

function Bus(plate) {
  this.plate = plate;
  this.faults = [];        // a fresh array for every object
}
Bus.prototype.report = function (what) { this.faults.push(what); return this; };

const a = new Bus('LAG-441-XA');
const b = new Bus('ABJ-772-KM');
a.report('cracked windscreen');

console.log('a.faults:', a.faults);
console.log('b.faults:', b.faults);
console.log('same array?', a.faults === b.faults);

Output


      

Delegation without any constructors at all

Object.create lets you build a chain directly, with no functions involved. This is sometimes called the delegation pattern, and it is worth seeing once because it shows how little machinery prototypes actually need.

Behaviour on a shared object, data on the instances

const busBehaviour = {
  describe() {
    return `${this.plate} has ${this.seatsLeft} seats left`;
  },
  book(n) {
    this.seatsLeft -= n;
    return this;
  },
};

function spawn(plate, seatsLeft) {
  const obj = Object.create(busBehaviour);
  obj.plate = plate;
  obj.seatsLeft = seatsLeft;
  return obj;
}

const a = spawn('LAG-441-XA', 9);
const b = spawn('ABJ-772-KM', 6);

console.log(a.book(2).describe());
console.log(b.describe());
console.log('shared behaviour:', a.describe === b.describe);
console.log('own keys:', Object.keys(a));

Output


      

An object with nothing above it

const dict = Object.create(null);
dict.toString = 'just a string, no conflict';

console.log(dict.toString);
console.log('prototype:', Object.getPrototypeOf(dict));

try {
  console.log(String(dict));
} catch (err) {
  console.log('Cannot stringify:', err.message);
}

Output


      

Your turn: trace before you run

Predict every log below, then run it. For each one, say which level it was found on: the object, the prototype, or Object.prototype.

Editable. Predict first, then run it.

Output


      

Recap

  • Every object links to another object, its prototype, and property lookup walks that chain upward until it hits null.
  • Ctor.prototype is the object instances will hang from; Object.getPrototypeOf(obj) is what an object actually hangs from.
  • An own property shadows an inherited one, and assignment always writes to the object itself.
  • Never store arrays or objects on a prototype unless you intend every instance to share the same one.
  • Object.create(proto) builds a chain with no constructor functions at all; Object.create(null) builds an object with no chain.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.