JavaScript Advanced #3 Prototype Chain
JavaScript’s object model differs from other OOP languages. It’s not class-based but prototype-based. ES2015’s class syntax is sugar layered on top of prototypes. This post looks under the hood.
Every object refers to another object #
Every JavaScript object has an internal slot called [[Prototype]]. It points to another object.
const obj = { a: 1 };
Object.getPrototypeOf(obj); // Object.prototype
An empty object {}’s prototype is the built-in Object.prototype. It carries methods like toString, hasOwnProperty.
const obj = { a: 1 };
obj.toString(); // '[object Object]'
// ↑ not directly on obj — present on Object.prototype
Looking up obj.toString — JavaScript first checks obj itself, and if it’s missing, walks up the prototype chain. It reaches Object.prototype.toString and finds it there.
That’s a prototype chain lookup. Almost every method call in JavaScript happens via this.
A function’s prototype property
#
Every function has a prototype property.
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`hi, ${this.name}`);
};
const u = new User('Curtis');
u.greet(); // hi, Curtis
When new User('Curtis') runs:
- Create a new empty object
- Set its
[[Prototype]]toUser.prototype - Run
User’s body, adding properties tothis - Return the object
So when calling u.greet — it’s not on u itself, but is found on User.prototype via the chain.
Class = sugar over prototypes #
Rewriting the same code with ES2015’s class:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`hi, ${this.name}`);
}
}
const u = new User('Curtis');
console.log(u); // User { name: 'Curtis' }
console.log(u.greet); // [Function: greet]
Internally exactly the same thing happens.
class User {
greet() { /* ... */ }
}
// equivalent to
function User() {}
User.prototype.greet = function() { /* ... */ };Since class arrived, attaching methods directly to prototype rarely shows up in code, but the underlying structure is the same.
Prototype chain — going deeper #
Inheritance is expressed as a longer chain.
class Animal {
speak() { console.log('sound'); }
}
class Dog extends Animal {
bark() { console.log('woof'); }
}
const d = new Dog();
d.bark(); // 'd' → found on 'Dog.prototype'
d.speak(); // 'd' → 'Dog.prototype' (none) → found on 'Animal.prototype'
Chain:
d
↓ [[Prototype]]
Dog.prototype (bark)
↓ [[Prototype]]
Animal.prototype (speak)
↓ [[Prototype]]
Object.prototype (toString, hasOwnProperty)
↓
nullProperty lookup walks up this chain. If not found at the end, undefined.
The end is null
#
Object.getPrototypeOf(Object.prototype); // null
Object.prototype’s prototype is null. The chain ends there.
Own vs inherited properties #
class User {
constructor(name) {
this.name = name;
}
greet() {}
}
const u = new User('Curtis');
Object.hasOwn(u, 'name'); // true — own instance property
Object.hasOwn(u, 'greet'); // false — on the prototype
'greet' in u; // true ('in' checks the chain)
Object.hasOwn checks own properties only. The in operator walks the chain. The difference matters occasionally.
Older code uses obj.hasOwnProperty('key') — but it can break if obj itself defines hasOwnProperty. ES2022’s Object.hasOwn is the safe answer.
Tools that work with prototypes directly #
1) Object.create — create with an explicit prototype
#
const animalProto = {
speak() { console.log('sound'); },
};
const dog = Object.create(animalProto);
dog.bark = function() { console.log('woof'); };
dog.speak(); // sound (from prototype)
dog.bark(); // woof (own property)
A way to set the prototype directly without a class. Common in old material; in modern code class is standard.
2) Object.create(null) — object with no prototype
#
const dict = Object.create(null);
dict.toString; // undefined — no Object.prototype either
const normal = {};
normal.toString; // [Function: toString] — built-ins visible
When using objects as key-value dictionaries — Object.create(null) is safe to avoid built-in key collisions like toString. In modern JavaScript Map is the better answer, but you’ll see this inside library code occasionally.
Modifying prototypes — not recommended #
Array.prototype.first = function() {
return this[0];
};
[1, 2, 3].first(); // 1
Adding methods to a built-in’s prototype (so-called monkey patching) is very powerful but almost always causes accidents. Easy to clash with other code (libraries, new standard methods).
Adding methods to your own class’s prototype is normal and fine. Avoid touching built-ins.
__proto__ — old behavior
#
Old JavaScript had a non-official __proto__ property. ES2015 standardized it but it’s not recommended.
const obj = {
__proto__: animalProto, // don't use
};
// recommended
const obj2 = Object.create(animalProto);Object.getPrototypeOf / Object.setPrototypeOf / Object.create are the modern answers.
instanceof — checking the prototype chain
#
class Animal {}
class Dog extends Animal {}
const d = new Dog();
d instanceof Dog; // true
d instanceof Animal; // true (parent via the chain)
d instanceof Object; // true
x instanceof C checks whether C.prototype exists somewhere on x’s prototype chain. Another application of chain lookup.
Functions are objects too — can have own properties #
In JavaScript, functions are also objects. You can attach properties directly.
function counter() {
counter.count = (counter.count ?? 0) + 1;
return counter.count;
}
counter(); // 1
counter(); // 2
counter(); // 3
This pattern isn’t used often, but it shows that JavaScript functions are first-class objects.
Concrete example — array’s method chain #
const arr = [1, 2, 3];
Object.hasOwn(arr, 'map'); // false
Object.hasOwn(arr, 'length'); // true
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
Chain:
[1, 2, 3] (length, 0, 1, 2)
↓
Array.prototype (map, filter, reduce, ...)
↓
Object.prototype (toString, ...)
↓
nullCalling arr.map — it’s not on arr itself, but is found and invoked on Array.prototype. Every array shares the same Array.prototype, so the method exists in memory only once.
Wrap-up #
What we covered:
- JavaScript is prototype-based — class syntax is sugar
- Every object’s
[[Prototype]]points to another object - Property lookup walks up the chain
- A function’s
prototypebecomes the new instance’s[[Prototype]]onnew Object.hasOwnchecks own properties (inchecks the chain)Object.create(...)creates with an explicit prototype- Don’t modify built-in prototypes
instanceofchecks the prototype chain
In the next post (#4 Event Loop and Tasks) we cover how JavaScript async actually works — the event loop, the microtask vs macrotask difference, and why Promise.then and setTimeout run in different orders.