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.
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
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.
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.
Bus.prototypeis a property on the constructor function. It is the object that instances will hang from. It is not the prototype ofBusitself.Object.getPrototypeOf(b)is the object thatbactually hangs from. For an instance ofBus, that isBus.prototype.b.__proto__is an old, deprecated accessor for the same thing. You will see it in older code and in browser consoles. Read it, do not write it.
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.
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
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
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.
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
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.
Output
Recap
- Every object links to another object, its prototype, and property lookup walks that chain upward until it hits
null. Ctor.prototypeis 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.