Skip to main content
JavaScript intermediate Lesson 13 of 24

Prototypes and Prototype Chain in JavaScript

Understand how JavaScript's prototype chain works, prototype-based inheritance, and how ES6 classes relate to prototypes under the hood.

JavaScript is a prototype-based language. Every object has an internal link to another object called its prototype. Property lookups walk this chain until the value is found or the chain ends at null. Understanding this mechanism is essential because it underpins everything from method inheritance to the class syntax introduced in ES6.

The Prototype Chain

When you access a property on an object, JavaScript first looks at the object itself. If not found, it walks up the prototype chain — checking each linked object in sequence. This delegation model means objects can share behavior without copying it. Every method you call on an array, for example, lives on Array.prototype, not on the array itself.

const animal = {
  breathe() {
    return `${this.name} is breathing`;
  },
};

const dog = Object.create(animal); // dog's prototype is animal
dog.name = "Rex";

console.log(dog.breathe());        // "Rex is breathing" — found on animal via chain
console.log(dog.hasOwnProperty("name"));    // true  — own property
console.log(dog.hasOwnProperty("breathe")); // false — inherited from animal
console.log(Object.getPrototypeOf(dog) === animal); // true

The chain here is: doganimalObject.prototypenull.

__proto__ vs prototype

These two properties are often confused because they sound similar but serve different purposes. prototype is a property on constructor functions — it is the object that gets assigned as __proto__ on any instance created with new. __proto__ is the actual prototype link on an object instance, and it is what gets walked during property lookup.

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

// `Person.prototype` is the object that becomes __proto__ of instances
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

const alice = new Person("Alice");

// On instances, use Object.getPrototypeOf() — __proto__ is deprecated
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice.__proto__ === Person.prototype);              // true (avoid in prod)

console.log(alice.greet()); // "Hi, I'm Alice"

Rule of thumb: use Object.getPrototypeOf() to read a prototype and Object.create() to set one. Avoid writing to __proto__ directly.

Object.create for Clean Inheritance

Object.create(proto) creates a new object whose prototype is proto. This is the most explicit and readable way to establish a prototype relationship — you can see exactly what you’re inheriting from without the noise of new and constructor wiring.

const vehicleProto = {
  describe() {
    return `${this.make} ${this.model} (${this.year})`;
  },
  start() {
    return `${this.make} engine started`;
  },
};

function createCar(make, model, year) {
  // Create a new object that delegates to vehicleProto
  const car = Object.create(vehicleProto);
  car.make = make;
  car.model = model;
  car.year = year;
  return car;
}

const tesla = createCar("Tesla", "Model 3", 2024);
console.log(tesla.describe()); // "Tesla Model 3 (2024)"
console.log(tesla.start());    // "Tesla engine started"

Prototype-Based Inheritance with Constructor Functions

Before ES6 classes, this constructor function pattern was the standard way to implement inheritance in JavaScript. It is still important to understand because it reveals what the class syntax is doing under the hood, and because you will encounter it in older codebases.

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

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

// Wire up the prototype chain — Dog instances delegate to Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // restore constructor reference (gets overwritten above)

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

const rex = new Dog("Rex", "Labrador");
console.log(rex.speak()); // "Rex makes a sound"  — inherited from Animal.prototype
console.log(rex.bark());  // "Rex barks!"          — own method on Dog.prototype
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

How ES6 Classes Relate to Prototypes

ES6 class syntax is syntactic sugar over the constructor function pattern above. The JavaScript engine compiles it to the same prototype-based mechanism — there are no new runtime semantics. Classes are cleaner to write and read, but understanding prototypes means you can debug class-based code at the prototype level when needed.

class Animal {
  constructor(name) {
    this.name = name;
  }
  // speak lives on Animal.prototype — shared by all instances
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // calls Animal's constructor
    this.breed = breed;
  }
  bark() {
    return `${this.name} barks!`;
  }
}

const rex = new Dog("Rex", "Labrador");

// Verify the prototype chain is identical to the manual approach above
console.log(typeof Dog);                  // "function" — classes are constructor functions
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Dog.prototype.hasOwnProperty("bark"));         // true — bark is on the prototype

Prototype Methods vs Instance Methods

Methods defined inside the constructor body are re-created as new function objects for every instance. Methods defined on the prototype (or in the class body, which goes to the prototype) are created once and shared. For classes with many instances, keeping methods on the prototype significantly reduces memory usage.

class Counter {
  constructor(start = 0) {
    this.count = start;

    // BAD: a new function object is allocated for every Counter instance
    this.reset = function () {
      this.count = 0;
    };
  }

  // GOOD: increment and value are defined once on Counter.prototype
  increment() {
    this.count++;
  }

  value() {
    return this.count;
  }
}

const a = new Counter();
const b = new Counter();

// Prototype methods share a single function reference
console.log(a.increment === b.increment); // true — same function object
// Instance methods are distinct per instance
console.log(a.reset === b.reset);         // false — two separate function objects

For a class with thousands of instances, putting methods on the prototype instead of the constructor measurably reduces memory usage.

Prototype Pollution — A Real Danger

Prototype pollution occurs when untrusted input is merged into Object.prototype. Because every plain object inherits from Object.prototype, adding a property there affects all objects in the application — a subtle but critical security vulnerability that has affected many popular libraries.

// Vulnerable pattern — never merge untrusted data without guards
function merge(target, source) {
  for (const key in source) {
    target[key] = source[key]; // no hasOwnProperty guard!
  }
}

const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

console.log({}.isAdmin); // true — every object in the app is now "admin"!

Safe alternatives:

// Option 1: guard with hasOwnProperty — only copy own properties, not inherited ones
for (const key in source) {
  if (Object.prototype.hasOwnProperty.call(source, key)) {
    target[key] = source[key];
  }
}

// Option 2: use Object.create(null) for lookup tables — no prototype chain to pollute
const safe = Object.create(null); // safe has no __proto__, no toString, no hasOwnProperty
safe.name = "ok";

Key Takeaways

  • Every object has a prototype; lookups walk the chain until null.
  • Object.create(proto) is the cleanest way to establish inheritance.
  • Use Object.getPrototypeOf() instead of __proto__.
  • ES6 classes compile to the same prototype mechanism — no magic.
  • Never mutate Object.prototype; always guard against prototype pollution.

Frequently Asked Questions

What is the difference between __proto__ and prototype?
__proto__ is a property on every object instance that points to its prototype (the object it inherits from). prototype is a property on constructor functions — it becomes the __proto__ of any object created with that constructor. Use Object.getPrototypeOf() instead of __proto__ in production code.
How is prototype-based inheritance different from class-based inheritance?
In classical inheritance, classes are blueprints copied to instances. In prototype-based inheritance, objects delegate property lookups to a shared prototype object at runtime. JavaScript classes (ES6+) are syntactic sugar over the same prototype mechanism.
What is prototype pollution and why is it dangerous?
Prototype pollution is when an attacker or bug causes Object.prototype to be mutated. Because every plain object inherits from Object.prototype, adding a property there affects all objects in the application, leading to unexpected behavior or security vulnerabilities.