Skip to main content

Command Palette

Search for a command to run...

Prototype - The Backbone of JavaScript Objects

Updated
6 min read

JavaScript's prototype system is one of its most powerful yet misunderstood features. Unlike classical object-oriented languages that use classes as blueprints, JavaScript uses prototypes to implement inheritance and share functionality between objects. Let's dive deep into this fundamental concept.

Understanding Prototypes in JavaScript

The Blueprint Analogy

Think of prototypes like architectural blueprints for houses. When you build a house, you don't recreate the blueprint each time—you reference the original design. Similarly, when you create objects in JavaScript, they don't duplicate all methods and properties. Instead, they reference a shared prototype object.

// When you create an array
const myArray = [1, 2, 3];

// It doesn't copy methods like push, pop, map
// Instead, it links to Array.prototype
console.log(myArray.__proto__ === Array.prototype); // true

// So when you call a method
myArray.push(4);
// JavaScript looks up the prototype chain to find 'push'

What Exactly is a Prototype?

Every JavaScript object has an internal property called [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf()). This property is a reference to another object, forming a chain that JavaScript traverses when looking for properties or methods.

const person = {
  greet() {
    return `Hello, I'm ${this.name}`;
  }
};

const john = Object.create(person);
john.name = 'John';

console.log(john.greet()); // "Hello, I'm John"
// john doesn't have greet(), but its prototype does!

Constructor Functions and the prototype Property

When you create a constructor function, JavaScript automatically gives it a prototype property. This property becomes the prototype of all instances created with that constructor.

function Car(brand, model) {
  this.brand = brand;
  this.model = model;
}

// Adding methods to the prototype
Car.prototype.getInfo = function() {
  return `${this.brand} ${this.model}`;
};

const tesla = new Car('Tesla', 'Model 3');
const bmw = new Car('BMW', 'X5');

console.log(tesla.getInfo()); // "Tesla Model 3"
console.log(bmw.getInfo()); // "BMW X5"

// Both instances share the same method
console.log(tesla.getInfo === bmw.getInfo); // true

The Prototype Chain - How Inheritance Works

The prototype chain is JavaScript's mechanism for inheritance. When you try to access a property on an object, JavaScript follows this lookup process:

  1. Check if the property exists on the object itself

  2. If not, check the object's prototype

  3. If not, check the prototype's prototype

  4. Continue until reaching Object.prototype

  5. If still not found, return undefined

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
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add Dog-specific method
Dog.prototype.bark = function() {
  return `${this.name} barks!`;
};

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

console.log(buddy.name);       // "Buddy" (own property)
console.log(buddy.bark());     // "Buddy barks!" (Dog.prototype)
console.log(buddy.speak());    // "Buddy makes a sound" (Animal.prototype)
console.log(buddy.toString()); // "[object Object]" (Object.prototype)

Visualizing the Prototype Chain

Classical vs Prototype-Based Inheritance

Classical InheritancePrototype-Based Inheritance
Classes are blueprintsObjects inherit from objects
Instances are copies of class structureInstances link to prototype objects
Inheritance through class hierarchyInheritance through prototype chain
Static structure defined at compile timeDynamic structure can change at runtime
Used in Java, C++, C#Used in JavaScript, Lua

Comparing Prototype Access Methods

JavaScript provides several ways to work with prototypes. Let's compare them:

1. Object.create()

The modern, recommended way to create objects with a specific prototype.

const animal = {
  type: 'Unknown',
  describe() {
    return `This is a ${this.type}`;
  }
};

const cat = Object.create(animal);
cat.type = 'Cat';

console.log(cat.describe()); // "This is a Cat"
console.log(Object.getPrototypeOf(cat) === animal); // true

Pros: Clean, explicit, widely supported
Use case: When you want to create an object that inherits from another object

2. __proto__ Property

Direct access to an object's prototype (deprecated but still widely used).

const dog = { name: 'Rex' };
const puppy = { age: 1 };

puppy.__proto__ = dog;
console.log(puppy.name); // "Rex"

// Better alternative:
Object.setPrototypeOf(puppy, dog);

Pros: Simple, direct access
Cons: Deprecated, performance issues
Use case: Debugging or legacy code only

3. prototype Property

Used with constructor functions to define shared methods.

function Vehicle(type) {
  this.type = type;
}

Vehicle.prototype.move = function() {
  return `${this.type} is moving`;
};

const car = new Vehicle('Car');
console.log(car.move()); // "Car is moving"

Pros: Memory efficient, traditional pattern
Use case: Constructor functions, before ES6 classes

Modern Approach with ES6 Classes

ES6 classes are syntactic sugar over prototypes:

class Shape {
  constructor(color) {
    this.color = color;
  }

  describe() {
    return `A ${this.color} shape`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

const redCircle = new Circle('red', 5);
console.log(redCircle.describe()); // "A red shape"
console.log(redCircle.getArea().toFixed(2)); // "78.54"

// Under the hood, it's still prototypes!
console.log(redCircle.__proto__ === Circle.prototype); // true

Modifying Prototypes - Do's and Don'ts

✅ Do's

1. Add methods to your own constructor prototypes

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

User.prototype.sayHello = function() {
  return `Hello, I'm ${this.name}`;
};

2. Use Object.create() for delegation

const methods = {
  log() { console.log(this.value); }
};

const obj = Object.create(methods);
obj.value = 42;

3. Check property ownership

const person = { name: 'Alice' };
person.__proto__.age = 30;

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age'));  // false

❌ Don'ts

1. Never modify built-in prototypes (except for polyfills)

// BAD - Don't do this!
Array.prototype.myMethod = function() {
  // This affects ALL arrays everywhere
};

// If you must (for polyfills), check first:
if (!Array.prototype.includes) {
  Array.prototype.includes = function(searchElement) {
    // Polyfill implementation
  };
}

2. Avoid setting proto in production

// BAD - Performance killer
obj.__proto__ = somePrototype;

// GOOD - Use these instead
Object.setPrototypeOf(obj, somePrototype);
// or better yet:
const obj = Object.create(somePrototype);

3. Don't create very long prototype chains

// BAD - Too many levels
A.prototype = Object.create(B.prototype);
B.prototype = Object.create(C.prototype);
C.prototype = Object.create(D.prototype);
D.prototype = Object.create(E.prototype);

// GOOD - Keep it shallow, use composition instead

Performance Considerations

// Slow: Property lookup travels up the chain
const deep = Object.create(
  Object.create(
    Object.create(
      Object.create({ value: 42 })
    )
  )
);

console.log(deep.value); // Has to traverse 4 levels

// Fast: Direct property access
const shallow = { value: 42 };
console.log(shallow.value); // Immediate access

Practical Examples

Example 1: Creating a Simple Plugin System

const Plugin = {
  init() {
    console.log(`${this.name} initialized`);
  }
};

function createPlugin(name, functionality) {
  const plugin = Object.create(Plugin);
  plugin.name = name;
  plugin.execute = functionality;
  return plugin;
}

const logger = createPlugin('Logger', function(msg) {
  console.log(`[LOG]: ${msg}`);
});

logger.init(); // "Logger initialized"
logger.execute('Hello World'); // "[LOG]: Hello World"

Example 2: Implementing Method Sharing

const mathOperations = {
  add(a, b) { return a + b; },
  subtract(a, b) { return a - b; }
};

function Calculator(name) {
  this.name = name;
  this.history = [];
}

Calculator.prototype = Object.create(mathOperations);
Calculator.prototype.calculate = function(operation, a, b) {
  const result = this[operation](a, b);
  this.history.push(`${operation}(${a}, ${b}) = ${result}`);
  return result;
};

const calc = new Calculator('MyCalc');
console.log(calc.calculate('add', 5, 3)); // 8
console.log(calc.history); // ["add(5, 3) = 8"]

Conclusion

Prototypes are the foundation of JavaScript's object model. Understanding them helps you:

  • Write more memory-efficient code by sharing methods

  • Understand how inheritance works in JavaScript

  • Debug complex object hierarchies

  • Make informed decisions about using classes vs prototypes

  • Avoid common pitfalls when extending objects

While ES6 classes provide a more familiar syntax, prototypes remain the underlying mechanism. Mastering prototypes gives you deeper insight into how JavaScript truly works under the hood.


Key Takeaways:

  • Every object has a prototype (except Object.prototype)

  • Prototypes enable inheritance through delegation

  • Use Object.create() for modern prototype manipulation

  • Avoid modifying built-in prototypes

  • ES6 classes are syntactic sugar over prototypes

  • Keep prototype chains shallow for better performance

Happy coding! 🚀