Prototypes & Prototype Chain

Difficulty: Intermediate

Every object in JavaScript has a hidden internal property called [[Prototype]] that points to another object - its prototype. This is the foundation of JavaScript's inheritance model, which is fundamentally different from class-based languages like Java or C++. When you access a property on an object, JavaScript first looks at the object itself. If the property is not found, the engine walks up the prototype chain - checking the object's prototype, then that prototype's prototype, and so on - until it either finds the property or reaches an object whose prototype is null (typically Object.prototype, whose own [[Prototype]] is null).

There is an important distinction between __proto__ and the prototype property. The __proto__ property (now standardized as Object.getPrototypeOf()) is a getter/setter that exists on every object and points to the object's actual prototype - the object it inherits from. The prototype property, on the other hand, only exists on functions. When a function is used as a constructor with the new keyword, the newly created object's __proto__ is set to the constructor function's prototype property. This is how methods defined on Constructor.prototype become available to all instances.

Object.create() is the most direct way to set up prototypal inheritance. It creates a new object with its [[Prototype]] set to the object you pass as the first argument. This gives you fine-grained control without involving constructor functions or the new keyword. It is frequently used when you want clean delegation chains or when implementing inheritance between constructor functions.

Constructor functions were the original pattern for creating objects with shared behavior in JavaScript. When you call a function with new, four things happen: a new empty object is created, its __proto__ is linked to the constructor's prototype, the constructor runs with this bound to the new object, and the object is returned (unless the constructor explicitly returns a different object). All instances created by the same constructor share methods defined on the constructor's prototype, which is memory-efficient compared to defining methods inside the constructor.

The hasOwnProperty method and the in operator behave differently when it comes to inherited properties. hasOwnProperty returns true only if the property exists directly on the object, ignoring the prototype chain. The in operator returns true if the property exists anywhere in the prototype chain. Understanding this distinction is critical when iterating over object properties - for...in loops walk the entire chain, which is why you often see hasOwnProperty checks inside them.

Performance-wise, keeping prototype chains short is generally better. Deep chains mean more lookups when resolving properties. In practice, most chains are only two or three levels deep (instance -> Constructor.prototype -> Object.prototype), so this rarely becomes a bottleneck. However, relying heavily on prototype chain lookups for frequently accessed properties in hot code paths can have a measurable impact. Caching looked-up values in local variables or using direct property assignment are common optimizations.

Code examples

Prototype chain and property lookup

const animal = {
  eats: true,
  walk() {
    return 'Animal walks';
  }
};

const rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.jumps);    // own property
console.log(rabbit.eats);     // inherited from animal
console.log(rabbit.walk());   // inherited method

console.log(rabbit.__proto__ === animal);           // true
console.log(Object.getPrototypeOf(rabbit) === animal); // true

// Prototype chain: rabbit -> animal -> Object.prototype -> null
console.log(rabbit.__proto__.__proto__ === Object.prototype); // true
console.log(rabbit.__proto__.__proto__.__proto__);            // null

rabbit does not have 'eats' or 'walk' directly. JavaScript walks up to animal (its prototype) and finds them there. The chain continues to Object.prototype and finally to null.

Constructor functions and prototype

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  return `Hi, I'm ${this.name} and I'm ${this.age}`;
};

Person.prototype.species = 'Homo sapiens';

const alice = new Person('Alice', 25);
const bob = new Person('Bob', 30);

console.log(alice.greet());
console.log(bob.greet());
console.log(alice.species);

// Both share the same prototype object
console.log(alice.__proto__ === bob.__proto__);
console.log(alice.__proto__ === Person.prototype);

// greet is not on the instance itself
console.log(alice.hasOwnProperty('greet'));
console.log(alice.hasOwnProperty('name'));

Methods on Person.prototype are shared across all instances, saving memory. Instance properties like name and age are unique to each object. hasOwnProperty confirms that greet lives on the prototype, not the instance.

Object.create for inheritance between constructors

function Shape(color) {
  this.color = color;
}

Shape.prototype.describe = function() {
  return `A ${this.color} shape`;
};

function Circle(color, radius) {
  Shape.call(this, color);  // call parent constructor
  this.radius = radius;
}

// Set up inheritance: Circle.prototype inherits from Shape.prototype
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;  // fix constructor reference

Circle.prototype.area = function() {
  return Math.PI * this.radius * this.radius;
};

const c = new Circle('red', 5);
console.log(c.describe());
console.log(c.area().toFixed(2));
console.log(c instanceof Circle);
console.log(c instanceof Shape);
console.log(c.constructor === Circle);

Object.create(Shape.prototype) creates a new object whose __proto__ is Shape.prototype. This becomes Circle's prototype, establishing the inheritance chain. Shape.call(this, color) ensures the parent constructor initializes instance properties.

hasOwnProperty vs in operator

const base = { inherited: true };
const obj = Object.create(base);
obj.own = 'mine';

console.log(obj.hasOwnProperty('own'));        // direct property
console.log(obj.hasOwnProperty('inherited'));  // inherited, not own
console.log('own' in obj);                     // found on obj
console.log('inherited' in obj);               // found via chain
console.log('toString' in obj);                // from Object.prototype
console.log(obj.hasOwnProperty('toString'));   // not own

// Safe iteration with hasOwnProperty
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(`Own: ${key}`);
  } else {
    console.log(`Inherited: ${key}`);
  }
}

hasOwnProperty only returns true for properties directly on the object. The in operator checks the entire prototype chain. This distinction matters in for...in loops where you usually want to skip inherited properties.

Key points

Concepts covered

prototype chain, __proto__, Object.create, constructor functions, hasOwnProperty, prototypal inheritance