JavaScript Prototypes

Every object in JavaScript has a hidden link to another object called its prototype, and that prototype can have its own prototype, forming a chain. When you access a property or method that an object doesn’t have directly, JavaScript walks up this chain looking for it. This single mechanism is what powers inheritance in JavaScript, including the familiar class syntax, which is really just a friendlier way to write prototype-based code.

Overview: How Prototypes Work

Unlike class-based languages such as Java or C++, JavaScript uses prototypal inheritance. There are no blueprints that get copied into every instance. Instead, objects inherit directly from other live objects. Every object has an internal, hidden property (specified as [[Prototype]]) that points to another object or to null. This internal slot is exposed in most engines through the accessor __proto__, and can be read or set safely through Object.getPrototypeOf() and Object.setPrototypeOf().

When you write myObject.property, the engine does not just look at myObject. It performs a lookup: check myObject itself for an own property named property. If it’s not found, check myObject‘s prototype. If still not found, check that object’s prototype, and so on, until it reaches an object whose prototype is null (the end of the chain, which for ordinary objects is Object.prototype). If the property is never found, the result is undefined. This lookup is what makes methods like .toString() or .hasOwnProperty() “magically” available on every plain object — they live on Object.prototype, not on your object.

Functions play a special role here. Every regular function (declared with function) automatically gets a prototype property, which is a plain object. This is not the function’s own prototype (that would be Function.prototype) — it’s the object that will become the [[Prototype]] of any instance created with new FunctionName(). This is exactly how constructor functions and, under the hood, ES6 classes implement inheritance and shared methods without duplicating them on every instance.

Syntax

There are three common ways to work with prototypes: constructor functions, Object.create(), and the ES6 class syntax (which is prototype-based sugar). The snippet below shows the core syntax forms you’ll use to create and inspect prototype relationships.

// Constructor function pattern
function Ctor(param) {
  this.prop = param;
}
Ctor.prototype.method = function () {
  return this.prop;
};

// Object.create pattern
const proto = { greet() { return 'hi'; } };
const obj = Object.create(proto);

// Inspecting the chain
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, proto);
obj.__proto__;
Form Purpose
Ctor.prototype The object that becomes the prototype of every instance created via new Ctor().
Object.create(proto) Creates a brand-new object whose prototype is set to proto directly, with no constructor involved.
Object.getPrototypeOf(obj) The standard, safe way to read an object’s prototype.
Object.setPrototypeOf(obj, proto) Changes an existing object’s prototype (works, but is slow and generally discouraged).
obj.__proto__ A legacy accessor for the same thing as getPrototypeOf/setPrototypeOf; kept for web compatibility but not recommended in new code.
obj.hasOwnProperty(name) Checks whether a property exists directly on obj, ignoring the prototype chain.

Examples

Example 1: A constructor function with shared methods

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

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

const dog = new Animal('Rex');
dog.speak();

console.log(Object.getPrototypeOf(dog) === Animal.prototype);
console.log(dog.hasOwnProperty('name'));
console.log(dog.hasOwnProperty('speak'));
Rex makes a sound.
true
true
false

Only name is an own property of dog; it was assigned directly with this.name = name inside the constructor. The speak method lives on Animal.prototype, shared by every instance, which is why dog.hasOwnProperty('speak') is false even though dog.speak() works perfectly. This sharing is a major memory win: a thousand Animal instances all reuse the exact same speak function object instead of each carrying its own copy.

Example 2: Object.create() and the for…in loop

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

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

console.log(rabbit.eats);
console.log(rabbit.jumps);
rabbit.walk();

console.log(Object.getPrototypeOf(rabbit) === animal);
console.log(rabbit.hasOwnProperty('eats'));

for (const key in rabbit) {
  console.log(key);
}
true
true
Animal walks
true
false
jumps
eats
walk

Object.create(animal) builds rabbit with animal installed directly as its prototype — no constructor function or new keyword needed. Reading rabbit.eats and calling rabbit.walk() both succeed via the prototype chain. Notice the for...in loop: it walks own enumerable properties first (jumps), then continues up the chain and picks up the inherited enumerable properties (eats, walk) too. This is a common surprise for beginners who expect for...in to only see “the object’s own stuff”.

Example 3: How class syntax uses prototypes

class Shape {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `This is a ${this.name}.`;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super('circle');
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

const c = new Circle(2);
console.log(c.describe());
console.log(c.area().toFixed(2));

console.log(Object.getPrototypeOf(Circle.prototype) === Shape.prototype);
console.log(Object.getPrototypeOf(c) === Circle.prototype);
console.log(c instanceof Circle, c instanceof Shape);
This is a circle.
12.57
true
true
true true

The class keyword looks nothing like prototype code, but it compiles down to exactly the same mechanism you saw in Example 1. Methods declared inside a class body (like describe and area) are installed on Circle.prototype/Shape.prototype, not on each instance. extends links Circle.prototype‘s own [[Prototype]] to Shape.prototype, so an instance of Circle can find describe by walking: cCircle.prototypeShape.prototype. That’s why both Object.getPrototypeOf checks and both instanceof checks come back true.

How It Works Step by Step (Under the Hood)

When the engine evaluates dog.speak() from Example 1, it performs roughly these steps:

  • Look at dog itself: does it have an own property called speak? No — its only own property is name.
  • Follow dog‘s internal [[Prototype]] link, which points to Animal.prototype (this link was set automatically the moment new Animal('Rex') ran).
  • Look at Animal.prototype: does it have an own property called speak? Yes — the function assigned earlier.
  • Call that function with this bound to dog (the object the call originated from), which is why this.name inside speak correctly resolves to 'Rex'.

If Animal.prototype hadn’t had speak, the engine would keep climbing to Animal.prototype‘s own prototype (which is Object.prototype by default), and finally to null, at which point the lookup gives up and returns undefined (or throws a TypeError if you try to call it as a function). Crucially, this inside a prototype method is always determined by how the method is called, not where it’s defined — call it as dog.speak() and this is dog; detach it into a bare variable and call it alone, and this is lost.

Common Mistakes

Mistake 1: Putting instance methods on the prototype as arrow functions

A very common beginner error is writing Counter.prototype.increment = () => { this.count++; }. Arrow functions do not have their own this — they capture this from the surrounding scope at the point they’re defined, which here is the module scope, not any future instance. Calling counter.increment() then silently operates on the wrong this instead of throwing a helpful error. Always use a regular function expression (or ES6 method shorthand inside a class) for methods that need their own this:

function Counter() {
  this.count = 0;
}

Counter.prototype.increment = function () {
  this.count++;
  console.log(this.count);
};

const counter = new Counter();
counter.increment();
counter.increment();
1
2

Mistake 2: Forgetting the new keyword

Calling a constructor function without new doesn’t create a new object linked to the prototype at all — in non-strict mode it silently attaches properties to the global object, and in strict mode (or inside a class) it throws. You can defensively detect this using new.target, which is only set when the function was invoked with new:

function Person(name) {
  if (!new.target) {
    throw new TypeError('Person must be called with new');
  }
  this.name = name;
}

const p = new Person('Ana');
console.log(p.name);

try {
  const bad = Person('Bob');
} catch (err) {
  console.log(err.message);
}
Ana
Person must be called with new

Note that ES6 classes solve this automatically: calling a class as a plain function (without new) always throws a TypeError, which is one of the reasons classes are recommended over raw constructor functions for new code.

Best Practices

  • Prefer class syntax for new code — it’s built on the same prototype mechanism but reads clearly and enforces correct usage (like requiring new).
  • Put shared, stateless methods on the prototype (or inside the class body), and keep per-instance data assigned with this.prop = value in the constructor — this avoids wasting memory recreating identical functions for every instance.
  • Use Object.create(null) when you want a truly “bare” object with no inherited properties at all — useful for dictionaries/maps where you don’t want accidental collisions with inherited names like toString.
  • Use Object.getPrototypeOf() and Object.setPrototypeOf() instead of the legacy __proto__ accessor; it’s the standardized, more predictable API.
  • Avoid changing an object’s prototype after creation with setPrototypeOf in hot code paths — it de-optimizes property lookups in most engines. Set the correct prototype at creation time instead (via Object.create or a constructor).
  • Never modify the prototypes of built-in objects (Array.prototype, Object.prototype, etc.) in production code — it can silently break third-party libraries and future language features that assume those prototypes are unmodified.
  • Use Object.hasOwn(obj, key) (or obj.hasOwnProperty(key)) when you specifically want to ignore inherited properties, such as before a for...in loop or when checking configuration objects.

Practice Exercises

  1. Write a constructor function Book(title, author) that stores both as own properties, and add a describe() method to its prototype that returns a string like "Moby Dick by Herman Melville". Create two books and verify with Object.getPrototypeOf() that they share the exact same describe function.
  2. Using Object.create(), build a vehicle object with a drive() method, then create a car object whose prototype is vehicle and which adds its own honk() method. Call both methods on car and confirm with hasOwnProperty which method is “own” and which is inherited.
  3. Rewrite the Book constructor from exercise 1 as an ES6 class named Novel that extends a base class Publication. Confirm that Object.getPrototypeOf(Novel.prototype) === Publication.prototype is true.

Summary

  • Every object has an internal [[Prototype]] link to another object (or null), forming a prototype chain.
  • Property lookups walk up this chain until the property is found or the chain ends at null.
  • Constructor functions expose a prototype object that becomes the [[Prototype]] of instances created with new.
  • Object.create(proto) creates an object with a specific prototype directly, with no constructor function involved.
  • ES6 class and extends are syntax sugar over the exact same prototype mechanism.
  • Use Object.getPrototypeOf/setPrototypeOf over the legacy __proto__, and avoid mutating built-in prototypes.