JavaScript Advanced #3 Prototype Chain

6 min read

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.

see the prototype
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.

properties not on you still work
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's prototype
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:

  1. Create a new empty object
  2. Set its [[Prototype]] to User.prototype
  3. Run User’s body, adding properties to this
  4. 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 syntax
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-prototype equivalence
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.

inheritance = extending the 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)
 ↓
null

Property lookup walks up this chain. If not found at the end, undefined.

The end is null #

end of the chain
Object.getPrototypeOf(Object.prototype);   // null

Object.prototype’s prototype is null. The chain ends there.

Own vs inherited properties #

own vs inherited
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 #

Object.create
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 #

truly clean dictionary (map)
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 #

modifying built-ins — never do this
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.

__proto__ — old way
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 #

instanceof
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.

attach properties to a function
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 #

where do array methods live?
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, ...)
   ↓
null

Calling 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 prototype becomes the new instance’s [[Prototype]] on new
  • Object.hasOwn checks own properties (in checks the chain)
  • Object.create(...) creates with an explicit prototype
  • Don’t modify built-in prototypes
  • instanceof checks 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.

X