Skip to content
By Yash KapureFrontend13 min readSeptember 15, 2025

JavaScript's Hidden Superpower: Prototypal Inheritance from __proto__ to class

Every JavaScript object has a hidden link to another object - its prototype. Understanding this chain demystifies class inheritance, explains method lookup, and prevents prototype pollution bugs.

Article focus

Prototype chain

powers every JS method lookup

Key takeaways

  • Every object has an internal [[Prototype]] link forming a chain; property lookups walk this chain.
  • Object.create(proto) creates an object with proto as its prototype - the cleanest way to set up delegation.
  • ES6 class syntax is purely syntactic sugar; under the hood it sets up the same prototype chain.
  • hasOwnProperty distinguishes own properties from inherited ones - critical for safe object iteration.
  • Prototype pollution is a real security vulnerability - never set properties on Object.prototype.

What Is Prototypal Inheritance?

Prototypal inheritance is a mechanism where objects directly inherit from other objects through a prototype chain. When a property is not found on an object, JavaScript automatically looks at the object's prototype, then the prototype's prototype, until it reaches null.

Unlike classical inheritance (Java, C++) where classes are blueprints that create instances, JavaScript objects are concrete things that can directly link to other concrete things. There are no classes in the engine - just objects linked together.

This model is fundamentally more flexible. You can change an object's prototype at runtime, mix in behavior from multiple sources, and create delegation chains that classical inheritance cannot express cleanly.

When you call array.push() or "hello".toUpperCase(), you are using prototypal inheritance. These methods are not on the array or string itself - they live on Array.prototype and String.prototype, reached via the prototype chain.

Object.create(): The Clean Way to Set Up Inheritance

Object.create(proto) creates a new object with proto as its [[Prototype]]. It is the most explicit and correct way to establish prototype delegation without constructor functions.

Douglas Crockford introduced Object.create to provide a clean API for prototypal inheritance. Before it, developers had to use constructor function hacks to set up prototype chains correctly.

Object.create(null) creates an object with no prototype at all - useful for creating pure hash maps that won't accidentally match properties from Object.prototype (like "constructor", "toString", "hasOwnProperty").

javascript

// Base object (the "class" in prototypal terms)
const Vehicle = {
  init(make, model) {
    this.make = make;
    this.model = model;
    return this;
  },
  describe() {
    return `${this.make} ${this.model}`;
  },
};

// Create Car that delegates to Vehicle
const Car = Object.create(Vehicle);
Car.honk = function() { return 'Beep!'; };

// Create an instance of Car
const myCar = Object.create(Car).init('Toyota', 'Camry');

console.log(myCar.describe()); // "Toyota Camry" - from Vehicle
console.log(myCar.honk());     // "Beep!" - from Car

// Chain: myCar -> Car -> Vehicle -> Object.prototype -> null
console.log(Object.getPrototypeOf(myCar) === Car);         // true
console.log(Object.getPrototypeOf(Car) === Vehicle);       // true

// Pure dictionary with no prototype pollution risk
const cache = Object.create(null);
cache['constructor'] = 'safe'; // won't collide with Object.prototype.constructor
console.log(Object.getPrototypeOf(cache)); // null

Constructor Functions: The Old Way

Constructor functions use the new keyword to create objects. new sets the object's prototype to the constructor's .prototype property and binds this to the new object.

Before ES6 classes, constructor functions were the standard way to create objects with shared methods. Methods are defined on Constructor.prototype so they are shared across all instances - not re-created per instance.

The new keyword does four things: creates a new empty object, sets its [[Prototype]] to Constructor.prototype, calls the constructor with this = new object, and returns the new object (unless the constructor explicitly returns another object).

javascript

// Constructor function (PascalCase by convention)
function Animal(name, sound) {
  // 'this' is the new object
  this.name = name;
  this.sound = sound;
}

// Methods on prototype - shared across ALL instances (not copied per instance)
Animal.prototype.speak = function() {
  return `${this.name} says ${this.sound}`;
};

const cat = new Animal('Cat', 'meow');
const dog = new Animal('Dog', 'woof');

// Both instances share the same speak function
console.log(cat.speak === dog.speak); // true - same function reference

// What 'new' does under the hood:
function newKeyword(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype); // set prototype
  const result = Constructor.apply(obj, args);       // call constructor
  return result instanceof Object ? result : obj;    // return new object
}

const cat2 = newKeyword(Animal, 'Cat2', 'purr');
console.log(cat2.speak()); // "Cat2 says purr"

ES6 Classes: Syntactic Sugar, Same Prototype Chain

The class keyword is purely syntactic sugar over constructor functions and prototype assignment. The engine still creates the same prototype chain - there are no real classes in JavaScript.

Class syntax is cleaner and more familiar to developers coming from Java or C#. But it is critical to understand that classes do not fundamentally change how JavaScript works. typeof MyClass is "function". Class instances still have a [[Prototype]] chain.

The extends keyword sets up prototype chain delegation between two constructor functions. super() in a constructor is essentially calling the parent constructor function with the child instance as this.

javascript

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

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

  // Static methods go on the constructor, not the prototype
  static create(name) {
    return new Animal(name);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // calls Animal.constructor
  }

  speak() {
    return `${this.name} barks.`; // overrides Animal.speak
  }
}

const d = new Dog('Rex');
console.log(d.speak());                   // "Rex barks."
console.log(d instanceof Dog);            // true
console.log(d instanceof Animal);         // true - prototype chain

// Under the hood, 'class' creates the same prototype structure:
console.log(typeof Dog);                  // "function"
console.log(Dog.prototype.constructor === Dog); // true
console.log(Object.getPrototypeOf(Dog) === Animal); // true (static chain)
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

hasOwnProperty vs Inherited Properties

hasOwnProperty returns true only for properties defined directly on the object, not inherited ones. Use it to safely iterate object properties or check existence without walking the prototype chain.

When you use for...in to iterate an object, it includes inherited enumerable properties. This can cause bugs when you only want own properties. Always use Object.keys() (own enumerable) or for...in with hasOwnProperty guard.

In modern JavaScript, Object.hasOwn(obj, key) is the preferred alternative to obj.hasOwnProperty(key) - it works even on objects created with Object.create(null) which don't have hasOwnProperty at all.

javascript

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

// for...in includes inherited enumerable properties
for (const key in child) {
  console.log(key); // "own", then "inherited"
}

// Safe iteration - only own properties
for (const key in child) {
  if (Object.hasOwn(child, key)) { // modern API
    console.log(key); // "own" only
  }
}

// Object.keys - only own enumerable properties (never inherited)
console.log(Object.keys(child));          // ["own"]
console.log(Object.getOwnPropertyNames(child)); // ["own"] including non-enumerable

// hasOwnProperty can be shadowed - use Object.hasOwn instead
const risky = Object.create(null);
risky.hasOwnProperty = () => true; // would break old pattern
// risky.hasOwnProperty('foo') - always true! Bug.

// Modern safe check:
console.log(Object.hasOwn(risky, 'foo')); // false - correct

Prototype Pollution: A Real Security Vulnerability

Prototype pollution occurs when attacker-controlled input adds properties to Object.prototype, affecting every object in the application. It enables XSS, logic bypasses, and remote code execution.

If user input is used to set properties on an object without validation, an attacker can send {"__proto__": {"isAdmin": true}} to add isAdmin to every object in the application. Code that checks if(user.isAdmin) will suddenly return true for all users.

This vulnerability has affected major libraries including lodash, jQuery, and Hoek. Always sanitize keys before using them in object assignment, and use Object.create(null) for caches/maps that accept external keys.

javascript

// VULNERABLE: naive deep merge with user input
function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object') {
      merge(target[key] ??= {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

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

// Now EVERY object has isAdmin = true!
const innocent = {};
console.log(innocent.isAdmin); // true - prototype was polluted!

// SAFE: Check for __proto__ and prototype keys
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // skip dangerous keys
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      safeMerge(target[key] ??= Object.create(null), source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

Composition Over Inheritance: The Modern Approach

Composition builds objects by combining small, focused behaviors. It avoids the fragile base class problem and the gorilla-banana problem of deep inheritance hierarchies.

The classic critique of inheritance: "you wanted a banana but got the gorilla holding the banana and the entire jungle." Deep inheritance hierarchies tightly couple your code and make changes unpredictable.

Composition says: instead of "is-a" (Dog extends Animal), use "has-a" (a dog object has swim behavior and bark behavior mixed in). Each behavior is a small object or function, composed together.

javascript

// Behaviors as mixins (objects or functions)
const canSwim = {
  swim() { return `${this.name} is swimming`; }
};
const canFly = {
  fly() { return `${this.name} is flying`; }
};
const canWalk = {
  walk() { return `${this.name} is walking`; }
};

// Compose: duck can do all three
function createDuck(name) {
  return Object.assign(
    { name },
    canSwim,
    canFly,
    canWalk
  );
}

// Compose: fish can only swim
function createFish(name) {
  return Object.assign({ name }, canSwim);
}

const duck = createDuck('Donald');
console.log(duck.swim()); // "Donald is swimming"
console.log(duck.fly());  // "Donald is flying"

// No inheritance hierarchy - just composed capabilities
// Adding new behavior: just create a new mixin and mix it in

Recommended blogs

Continue reading

View all blogs
Abstract visualization of asynchronous JavaScript — glowing connected nodes representing Promise chains and async flow
Frontend
22 min readMay 1, 2026

Mastering Async JavaScript: Promises, Async/Await, and the Microtask Queue

From callback hell to Promise chains to async/await, this guide covers everything about asynchronous JavaScript including error handling, Promise combinators, AbortController, and real-world patterns.

Pexels — Free Stock Photo

Read article
Developer coding with AI assistance on a dark terminal screen
AI Tools
13 min readApril 28, 2026

How I Use Claude Code to Ship Features 10× Faster

A real-world workflow guide for using Claude Code CLI to build features, debug bugs, refactor legacy code, and run custom automations - with actual examples from building this portfolio.

Subscribe for new posts, or read how referrals and sponsored placements are handled on this site.