Skip to main content
Advertisement

5.2 Prototype Chain

JavaScript's inheritance is prototype-based, not class-based. Even the class syntax works on top of the prototype chain internally.


[[Prototype]] Internal Slot

Every JavaScript object has a hidden internal slot called [[Prototype]]. It either points to another object or is null.

const obj = { x: 1 };

// Accessing [[Prototype]]
// Method 1: __proto__ (non-standard, supported in browsers)
console.log(obj.__proto__ === Object.prototype); // true

// Method 2: Object.getPrototypeOf() (recommended)
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// Top of prototype chain
console.log(Object.getPrototypeOf(Object.prototype)); // null

// Visualizing the prototype chain
function getProtoChain(obj) {
const chain = [];
let current = obj;
while (current !== null) {
chain.push(current.constructor?.name ?? "Object");
current = Object.getPrototypeOf(current);
}
return chain.join(" → ");
}

function Animal(name) { this.name = name; }
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog = new Dog("Rex", "Jindo");
console.log(getProtoChain(dog));
// "Dog → Animal → Object"

Setting Prototypes with Object.create()

// Create an object with a specified prototype
const vehicleProto = {
start() {
return `${this.make} ${this.model} started`;
},
stop() {
return `${this.make} ${this.model} stopped`;
},
toString() {
return `[Vehicle: ${this.make} ${this.model}]`;
},
};

const car = Object.create(vehicleProto);
car.make = "Hyundai";
car.model = "Elantra";
car.year = 2024;

console.log(car.start()); // "Hyundai Elantra started"
console.log(Object.getPrototypeOf(car) === vehicleProto); // true

// Object.create's second argument: property descriptors
const car2 = Object.create(vehicleProto, {
make: { value: "Kia", writable: true, enumerable: true, configurable: true },
model: { value: "K5", writable: true, enumerable: true, configurable: true },
});

console.log(car2.start()); // "Kia K5 started"

// Inheritance using prototype chain
const electricVehicleProto = Object.create(vehicleProto, {
charge: {
value: function() { return `${this.make} ${this.model} is charging`; },
writable: true, enumerable: true, configurable: true,
},
});

const ev = Object.create(electricVehicleProto);
ev.make = "Tesla";
ev.model = "Model 3";

console.log(ev.start()); // "Tesla Model 3 started" (inherited from vehicleProto)
console.log(ev.charge()); // "Tesla Model 3 is charging"

Property Lookup in the Prototype Chain

function Animal(name) {
this.name = name; // instance property
}

Animal.prototype.type = "animal"; // prototype property
Animal.prototype.breathe = function() { // prototype method
return `${this.name} is breathing`;
};

function Dog(name, breed) {
Animal.call(this, name); // call parent constructor
this.breed = breed;
}

// Set Dog's prototype to an Animal instance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // restore constructor

Dog.prototype.bark = function() {
return `${this.name} barks: Woof!`;
};

const dog = new Dog("Rex", "Jindo");

// Property lookup order:
// 1. dog instance → name, breed (found)
// 2. Dog.prototype → constructor, bark (found)
// 3. Animal.prototype → type, breathe (found)
// 4. Object.prototype → hasOwnProperty, toString, etc.
// 5. null → lookup ends

console.log(dog.name); // "Rex" (instance)
console.log(dog.breed); // "Jindo" (instance)
console.log(dog.type); // "animal" (Animal.prototype)
console.log(dog.bark()); // "Rex barks: Woof!" (Dog.prototype)
console.log(dog.breathe()); // "Rex is breathing" (Animal.prototype)
console.log(dog.toString()); // "[object Object]" (Object.prototype)

// Checking the chain
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

hasOwnProperty vs in Operator

const parent = { inherited: "parent value" };
const child = Object.create(parent);
child.own = "own value";

// in operator: searches the entire prototype chain
console.log("own" in child); // true (own)
console.log("inherited" in child); // true (prototype)
console.log("missing" in child); // false

// hasOwnProperty: only own properties
console.log(child.hasOwnProperty("own")); // true
console.log(child.hasOwnProperty("inherited")); // false

// Modern approach: Object.hasOwn() (ES2022, safer than hasOwnProperty)
console.log(Object.hasOwn(child, "own")); // true
console.log(Object.hasOwn(child, "inherited")); // false

// for...in vs Object.keys
const obj = Object.create({ protoKey: "proto value" });
obj.ownKey1 = "own value 1";
obj.ownKey2 = "own value 2";

// for...in: all enumerable properties (including inherited)
const forInKeys = [];
for (const key in obj) {
forInKeys.push(key);
}
console.log(forInKeys); // ["ownKey1", "ownKey2", "protoKey"]

// Filter to own only
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
console.log(`own: ${key}`);
}
}

// Object.keys: only own enumerable properties
console.log(Object.keys(obj)); // ["ownKey1", "ownKey2"]

Prototype Pollution Prevention

// ❌ Prototype pollution vulnerability (never do this)
const payload = JSON.parse('{"__proto__": {"admin": true}}');

// Vulnerable when using Object.assign
// Object.assign({}, payload); // adds admin: true to all objects!

// ✅ Safe merge — filter dangerous keys
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue; // skip dangerous keys
}
target[key] = source[key];
}
return target;
}

// ✅ Use null-prototype objects (immune to pollution)
const safeMap = Object.create(null);
safeMap.user = "Alice";
safeMap.__proto__ = "string"; // treated as a regular key (no actual prototype change)

console.log(Object.getPrototypeOf(safeMap)); // null

ES6 Classes and Prototypes

Classes are syntactic sugar over prototypes.

// ES6 class
class Animal {
constructor(name) {
this.name = name;
}

speak() {
return `${this.name} makes a sound`;
}

static create(name) {
return new Animal(name);
}
}

class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}

speak() {
return super.speak() + " Woof!";
}
}

// Verify the internal structure — still prototype-based
console.log(typeof Animal); // "function"
console.log(Animal.prototype.constructor === Animal); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

const dog = new Dog("Rex", "Jindo");
console.log(dog.speak()); // "Rex makes a sound Woof!"

// Adding methods to the prototype is still possible (not recommended)
Animal.prototype.eat = function(food) {
return `${this.name} eats ${food}`;
};

console.log(dog.eat("kibble")); // "Rex eats kibble" (Dog inherits)

Practical Example: Mixin Pattern for Multiple Inheritance

// Define functionality-based mixins
const Serializable = {
serialize() {
return JSON.stringify(this);
},
deserialize(json) {
return Object.assign(Object.create(Object.getPrototypeOf(this)), JSON.parse(json));
},
};

const Comparable = {
compareTo(other) {
if (this.id < other.id) return -1;
if (this.id > other.id) return 1;
return 0;
},
equals(other) {
return this.id === other.id;
},
};

const Timestamped = {
createdAt: null,
updatedAt: null,
touch() {
this.updatedAt = new Date().toISOString();
return this;
},
initTimestamp() {
this.createdAt = new Date().toISOString();
this.updatedAt = this.createdAt;
return this;
},
};

// Mixin application utility
function mixin(target, ...sources) {
Object.assign(target.prototype, ...sources);
return target;
}

class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
this.initTimestamp();
}
}

mixin(User, Serializable, Comparable, Timestamped);

const alice = new User(1, "Alice", "alice@example.com");
const bob = new User(2, "Bob", "bob@example.com");

console.log(alice.serialize());
// '{"id":1,"name":"Alice","email":"alice@example.com",...}'

console.log(alice.compareTo(bob)); // -1 (alice.id < bob.id)
console.log(alice.equals(bob)); // false

alice.touch();
console.log(alice.updatedAt !== alice.createdAt); // true (updated)

Pro Tips

Tip 1: Dynamically extending prototype methods

// Example of extending Array.prototype (avoid in production due to pollution risk)
// Prefer standalone functions or subclasses instead
class SmartArray extends Array {
sum() {
return this.reduce((acc, val) => acc + val, 0);
}
average() {
return this.sum() / this.length;
}
unique() {
return new SmartArray(...new Set(this));
}
}

const arr = new SmartArray(1, 2, 3, 4, 5, 3, 2, 1);
console.log(arr.sum()); // 21
console.log(arr.average()); // 2.625
console.log([...arr.unique()]); // [1, 2, 3, 4, 5]
console.log(arr.filter(x => x > 2) instanceof SmartArray); // true!

Tip 2: Prototype chain performance optimization

// Longer chains mean more expensive property lookups
// Cache frequently accessed inherited methods in local variables
class DeepChain extends Object {
constructor() {
super();
// Directly bind frequently called methods
this.hasOwnProp = Object.prototype.hasOwnProperty.bind(this);
}
}

// Or extract via destructuring
const { hasOwnProperty } = Object.prototype;
// Later use hasOwnProperty.call(obj, key)

Tip 3: Symbol.species to control derived class type

class SafeArray extends Array {
// Cause map, filter, etc. to return Array instead of SafeArray
static get [Symbol.species]() {
return Array;
}

safeMethod() {
return "SafeArray-only method";
}
}

const sa = new SafeArray(1, 2, 3);
const mapped = sa.map(x => x * 2); // map returns Array
console.log(mapped instanceof SafeArray); // false (returns Array)
console.log(mapped instanceof Array); // true
Advertisement