DEV Community

Tihomir Ivanov
Tihomir Ivanov

Posted on

Prototypes & Prototypal Inheritance: JavaScript's Secret Superpower (and Memory Saver)

If you've learned object-oriented programming in languages like Java or C++, JavaScript's inheritance model will feel alien at first. There are no "real" classes in JavaScript (even with the class syntax introduced in ES6). Instead, JavaScript uses prototypes — a powerful and memory-efficient mechanism that's often misunderstood.

Here's the truth: Understanding prototypes is the key to understanding how JavaScript objects really work.

The Golden Rule

In JavaScript, objects inherit from other objects through a prototype chain. Every object has an internal [[Prototype]] reference that points to another object, and property lookups traverse this chain until the property is found or the chain ends.

In simpler terms: Objects link to other objects.

Let's demystify this from the ground up.


Part 1: What is a Prototype?

Every JavaScript object has a hidden internal property called [[Prototype]] (you can access it via __proto__ or Object.getPrototypeOf()).

When you try to access a property on an object, JavaScript:

  1. Checks if the object has that property directly
  2. If not, checks the object's prototype
  3. If not, checks the prototype's prototype
  4. Continues until it finds the property or reaches null

This chain is called the prototype chain.


Basic Example

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

const rabbit = {
  jumps: true
};

// Set rabbit's prototype to animal
Object.setPrototypeOf(rabbit, animal);

console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats);  // true (inherited from animal)
rabbit.walk();             // "Animal walks" (inherited method)
Enter fullscreen mode Exit fullscreen mode

What happened?

  • rabbit doesn't have an eats property, so JavaScript looks at rabbit.__proto__ (which is animal)
  • It finds eats: true there
  • Same for the walk() method

Visualizing the Prototype Chain

rabbit = {
  jumps: true,
  __proto__: animal
}

animal = {
  eats: true,
  walk: function,
  __proto__: Object.prototype
}

Object.prototype = {
  toString: function,
  hasOwnProperty: function,
  ...
  __proto__: null
}
Enter fullscreen mode Exit fullscreen mode

When you call rabbit.toString(), JavaScript:

  1. Checks rabbit → not found
  2. Checks animal → not found
  3. Checks Object.prototypefound!

Part 2: Constructor Functions and prototype

Before ES6 classes, JavaScript used constructor functions to create objects with shared methods.

The Old Way: Constructor Functions

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

// Methods go on the prototype (shared by all instances)
Animal.prototype.walk = function() {
  console.log(`${this.name} walks`);
};

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

dog.walk(); // "Dog walks"
cat.walk(); // "Cat walks"

console.log(dog.walk === cat.walk); // true (same function!)
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Animal.prototype is an object that becomes the [[Prototype]] of all instances created with new Animal()
  • Methods on prototype are shared across all instances (memory efficient!)
  • Properties set in the constructor (this.name) are unique to each instance

Why Put Methods on the Prototype?

Wrong way (memory inefficient):

function Animal(name) {
  this.name = name;
  this.walk = function() { // New function created for EVERY instance
    console.log(`${this.name} walks`);
  };
}

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

console.log(dog.walk === cat.walk); // false (different functions!)
Enter fullscreen mode Exit fullscreen mode

Right way (memory efficient):

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

Animal.prototype.walk = function() { // One function shared by all
  console.log(`${this.name} walks`);
};

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

console.log(dog.walk === cat.walk); // true (same function!)
Enter fullscreen mode Exit fullscreen mode

Result: With 10,000 Animal instances, the prototype approach uses one walk function in memory, while the constructor approach creates 10,000 separate functions.


Part 3: __proto__ vs prototype

This is where confusion arises:

  • prototype is a property on constructor functions (e.g., Animal.prototype)
  • __proto__ (or [[Prototype]]) is an internal property on objects that points to their prototype
function Animal(name) {
  this.name = name;
}

const dog = new Animal('Dog');

console.log(Animal.prototype);        // Object with constructor and methods
console.log(dog.__proto__);           // Same as Animal.prototype
console.log(dog.__proto__ === Animal.prototype); // true
Enter fullscreen mode Exit fullscreen mode

Mental Model:

Animal (function)
  ├─ prototype (object)
       ├─ constructor: Animal
       └─ walk: function

dog (instance)
  ├─ name: 'Dog'
  └─ __proto__ → Animal.prototype
Enter fullscreen mode Exit fullscreen mode

Part 4: ES6 Classes (Syntactic Sugar)

ES6 introduced the class syntax, but under the hood, it's still prototypes:

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

  walk() { // Method on prototype
    console.log(`${this.name} walks`);
  }
}

const dog = new Animal('Dog');
dog.walk(); // "Dog walks"

console.log(typeof Animal); // "function" (it's still a constructor!)
console.log(dog.__proto__ === Animal.prototype); // true
Enter fullscreen mode Exit fullscreen mode

What class does:

  • constructor() → same as the old constructor function
  • Methods inside the class → added to Animal.prototype
  • new Animal() → creates an instance with __proto__ pointing to Animal.prototype

Inheritance with Classes

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

  walk() {
    console.log(`${this.name} walks`);
  }
}

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

  bark() {
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog('Buddy', 'Golden Retriever');

dog.walk(); // "Buddy walks" (inherited)
dog.bark(); // "Buddy barks" (own method)

console.log(dog.__proto__);                  // Dog.prototype
console.log(dog.__proto__.__proto__);        // Animal.prototype
console.log(dog.__proto__.__proto__.__proto__); // Object.prototype
Enter fullscreen mode Exit fullscreen mode

Prototype chain:

dog
  └─ __proto__: Dog.prototype
       └─ __proto__: Animal.prototype
            └─ __proto__: Object.prototype
                 └─ __proto__: null
Enter fullscreen mode Exit fullscreen mode

Part 5: Practical Prototype Patterns

Pattern 1: Checking Property Origin

const animal = { eats: true };
const rabbit = { jumps: true, __proto__: animal };

console.log('jumps' in rabbit); // true (own property)
console.log('eats' in rabbit);  // true (inherited)

console.log(rabbit.hasOwnProperty('jumps')); // true
console.log(rabbit.hasOwnProperty('eats'));  // false (not own)
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Iterating Over Own Properties

const animal = { eats: true };
const rabbit = { jumps: true, __proto__: animal };

for (let key in rabbit) {
  console.log(key); // "jumps", "eats" (includes inherited!)
}

// Only own properties:
for (let key in rabbit) {
  if (rabbit.hasOwnProperty(key)) {
    console.log(key); // "jumps"
  }
}

// Modern way:
console.log(Object.keys(rabbit)); // ["jumps"] (only own properties)
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Creating Objects Without Prototype

// Normal object has Object.prototype as prototype
const obj1 = {};
console.log(obj1.toString); // function (inherited from Object.prototype)

// Object without prototype (useful for dictionaries)
const obj2 = Object.create(null);
console.log(obj2.toString); // undefined (no prototype!)
Enter fullscreen mode Exit fullscreen mode

Use case: Pure dictionaries where you don't want inherited properties:

const settings = Object.create(null);
settings.toString = 'Custom setting'; // No conflict with Object.prototype.toString
Enter fullscreen mode Exit fullscreen mode

Part 6: Prototypes in React

While modern React doesn't heavily rely on prototypes (thanks to functional components and hooks), understanding prototypes helps when:

1. Working with Class Components (Legacy Code)

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => { // Arrow function (own property, not on prototype)
    this.setState({ count: this.state.count + 1 });
  }

  render() { // Method (on prototype)
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • Counter.prototype has render (shared across all instances)
  • increment is an instance property (arrow function, not inherited)
  • React.Component.prototype has setState, forceUpdate, etc.

Prototype chain:

counterInstance
  └─ __proto__: Counter.prototype
       └─ __proto__: React.Component.prototype
            └─ __proto__: Object.prototype
Enter fullscreen mode Exit fullscreen mode

2. Understanding Method Binding in Class Components

The problem:

class Button extends React.Component {
  handleClick() { // Regular method (on prototype)
    console.log(this); // undefined in strict mode!
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is undefined:

  • this.handleClick passes the function without binding
  • When React calls it, this is lost

Solutions:

// Solution 1: Arrow function (creates new instance property)
class Button extends React.Component {
  handleClick = () => {
    console.log(this); // Always correct
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

// Solution 2: Bind in constructor
class Button extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // Create bound version
  }

  handleClick() {
    console.log(this);
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

// Solution 3: Arrow function in JSX (creates new function every render!)
class Button extends React.Component {
  handleClick() {
    console.log(this);
  }

  render() {
    return <button onClick={() => this.handleClick()}>Click</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Extending Component Libraries

Some libraries use prototypes for extensibility:

import { Component } from 'react';

// Adding a method to all React components (not recommended!)
Component.prototype.log = function(message) {
  console.log(`[${this.constructor.name}] ${message}`);
};

class MyComponent extends Component {
  componentDidMount() {
    this.log('Mounted'); // Uses inherited method
  }

  render() {
    return <div>Hello</div>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Warning: Modifying built-in prototypes is generally not recommended in production code.


4. Functional Components Don't Use Prototypes

Modern React with hooks avoids prototype chains entirely:

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => { // Regular function, no prototype involved
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this works without prototypes:

  • Functions are first-class citizens in JavaScript
  • No need for this binding
  • Closures handle state access (see Closures article!)

Part 7: Common Prototype Gotchas

Gotcha 1: Mutating Prototype Properties

function Animal() {}
Animal.prototype.friends = []; // Shared array!

const dog = new Animal();
const cat = new Animal();

dog.friends.push('Buddy');

console.log(cat.friends); // ["Buddy"] - Oops!
Enter fullscreen mode Exit fullscreen mode

Why? The friends array is shared on the prototype. Mutating it affects all instances.

Fix: Initialize arrays/objects in the constructor:

function Animal() {
  this.friends = []; // Each instance gets its own array
}
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: Shadowing Prototype Properties

const animal = {
  eats: true
};

const rabbit = {
  __proto__: animal
};

console.log(rabbit.eats); // true (inherited)

rabbit.eats = false; // Creates OWN property, doesn't modify prototype

console.log(rabbit.eats);      // false (own property)
console.log(animal.eats);      // true (prototype unchanged)
console.log(rabbit.__proto__.eats); // true
Enter fullscreen mode Exit fullscreen mode

Key Point: Assigning to a property creates an own property, it doesn't modify the prototype.


Gotcha 3: Losing this in Callbacks

class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }

  log(message) { // Method on prototype
    console.log(`${this.prefix}: ${message}`);
  }
}

const logger = new Logger('INFO');
logger.log('Test'); // "INFO: Test"

setTimeout(logger.log, 1000, 'Test'); // Error: Cannot read 'prefix' of undefined
Enter fullscreen mode Exit fullscreen mode

Why? When you pass logger.log to setTimeout, it loses the this binding.

Fix:

setTimeout(() => logger.log('Test'), 1000); // Arrow function preserves context
// or
setTimeout(logger.log.bind(logger), 1000, 'Test'); // Bind explicitly
Enter fullscreen mode Exit fullscreen mode

Quick Reference Cheat Sheet

Concept Explanation Example
[[Prototype]] Internal property linking objects obj.__proto__
prototype Property on constructor functions Animal.prototype
Prototype Chain Series of __proto__ links obj → proto1 → proto2 → null
hasOwnProperty() Check if property is own (not inherited) obj.hasOwnProperty('key')
Object.create() Create object with specific prototype Object.create(proto)
Object.getPrototypeOf() Get object's prototype (modern) Object.getPrototypeOf(obj)
instanceof Check if object is in constructor's chain dog instanceof Animal

Key Takeaways

JavaScript uses prototypal inheritance, not classical inheritance
Methods on the prototype are shared across all instances (memory efficient)
Properties on the instance are unique to each object
__proto__ points to the object's prototype; prototype is on constructor functions
ES6 classes are syntactic sugar over constructor functions and prototypes
React class components use prototypes, but functional components don't
Never mutate shared arrays/objects on prototypes — initialize them in constructors
Arrow functions in class components create instance properties, not prototype methods


Interview Tip

When asked about prototypes, explain it in layers:

  1. "Every object has an internal prototype link (__proto__)"
  2. "When you access a property, JavaScript searches the prototype chain"
  3. "Constructor functions have a prototype property that becomes the __proto__ of instances"
  4. Give an example: "Methods on the prototype are shared for memory efficiency"
  5. React connection: "Class components inherit from React.Component.prototype, which provides setState and other methods"

Now go forth and never confuse __proto__ and prototype again!

Top comments (0)