Knowledge Base/concepts/JavaScript Prototypes and the Prototype Chain

JavaScript Prototypes and the Prototype Chain

JavaScript prototypes are the reason objects can use methods they do not own. When you read a property, JavaScript checks the object first, then walks up a linked chain of prototype objects until it finds the property or reaches the end. Class syntax did not replace that model. It dressed it in cleaner clothes.

If you understand property lookup, class, extends, instanceof, and a lot of "where did this method come from?" moments become much less mysterious.

Property lookup starts on the object

Start with a plain object:

const user = {
  name: "Ada",
};

console.log(user.name); // "Ada"

name is an own property. It lives directly on user.

Now call a method you did not define:

console.log(user.toString());
// "[object Object]"

user does not have its own toString property. JavaScript finds toString on Object.prototype, which is linked as the prototype of normal object literals.

That hidden link is the object's [[Prototype]].

The prototype chain

You can create a direct prototype link with Object.create:

const animal = {
  breathes: true,
};

const dog = Object.create(animal);
dog.legs = 4;

console.log(dog.legs); // 4
console.log(dog.breathes); // true

Lookup works like this:

  1. Check dog for legs.
  2. If the property is not there, check animal.
  3. If it is not there, keep walking upward.
  4. Stop at null.

If the property is never found, the result is undefined.

console.log(dog.wings); // undefined

This is prototypal inheritance: objects linked to other objects.

Reading an object's prototype

Use Object.getPrototypeOf:

Object.getPrototypeOf(dog) === animal; // true
Object.getPrototypeOf(animal) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype); // null

You may see __proto__ in older examples. Avoid it in new code. Object.getPrototypeOf is the clearer read API. Object.setPrototypeOf exists for changing a prototype, but changing prototypes after objects are created can hurt performance and make behavior harder to reason about.

Set the prototype at creation time when you can.

Object.create

Object.create(proto) makes a new object whose prototype is proto:

const sharedMethods = {
  describe() {
    return `${this.name} has ${this.wheels} wheels`;
  },
};

const bike = Object.create(sharedMethods);
bike.name = "Bike";
bike.wheels = 2;

console.log(bike.describe());
// "Bike has 2 wheels"

describe lives on sharedMethods. When called as bike.describe(), this is still bike, because this is determined by the call site.

You can also create an object with no prototype:

const dictionary = Object.create(null);
dictionary.course = "javascript";

console.log(dictionary.toString); // undefined

Objects with a null prototype are useful for pure dictionaries, because they do not inherit keys from Object.prototype.

Constructor functions and .prototype

Before class, constructor functions were the common object factory pattern:

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

User.prototype.greet = function () {
  return `Hello, ${this.name}`;
};

const ada = new User("Ada");

console.log(ada.greet()); // "Hello, Ada"

When you call new User("Ada"), JavaScript:

  1. Creates a new object.
  2. Sets that object's prototype to User.prototype.
  3. Calls User with this set to the new object.
  4. Returns the new object unless the constructor returns another object.

The confusing naming is unfortunate: a function's .prototype property is not the same thing as an object's internal [[Prototype]]. The function's .prototype becomes the internal prototype of instances created with new.

Object.getPrototypeOf(ada) === User.prototype; // true

Methods on prototypes are shared

If you define a method inside the constructor, every instance gets a separate function:

function User(name) {
  this.name = name;
  this.greet = function () {
    return `Hello, ${this.name}`;
  };
}

If you define it on the prototype, instances share one function:

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

User.prototype.greet = function () {
  return `Hello, ${this.name}`;
};

That is the main reason prototype methods exist: shared behavior without copying the method onto every object.

Class syntax uses prototypes

This class:

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

  greet() {
    return `Hello, ${this.name}`;
  }
}

still puts greet on User.prototype:

const ada = new User("Ada");

Object.getPrototypeOf(ada) === User.prototype; // true

class syntax is not fake, but it is not a different inheritance engine. It is a cleaner syntax over prototype-based mechanics.

With extends, the prototype chain connects subclasses to parent prototypes:

class Animal {
  speak() {
    return "sound";
  }
}

class Dog extends Animal {
  speak() {
    return "bark";
  }
}

Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true

If a method is not found on Dog.prototype, lookup continues to Animal.prototype.

Own properties shadow prototype properties

An own property wins over a prototype property with the same name:

const defaults = {
  role: "viewer",
};

const user = Object.create(defaults);

console.log(user.role); // "viewer"

user.role = "admin";

console.log(user.role); // "admin"
console.log(defaults.role); // "viewer"

The prototype property was not changed. It was shadowed. Delete the own property and the prototype value appears again:

delete user.role;

console.log(user.role); // "viewer"

instanceof checks the chain

instanceof checks whether a constructor's .prototype appears anywhere in an object's prototype chain.

class Animal {}
class Dog extends Animal {}

const rover = new Dog();

console.log(rover instanceof Dog); // true
console.log(rover instanceof Animal); // true

It does not check whether the constructor literally created the object in some permanent historical record. It checks the current prototype relationship.

That means changing prototypes can make instanceof surprising:

function Widget() {}

const widget = new Widget();

Widget.prototype = {};

console.log(widget instanceof Widget); // false

The object still has its original prototype. The constructor now points at a different .prototype object.

Prototype pollution

Prototype pollution happens when untrusted input manages to modify a shared prototype. Once that happens, many unrelated objects can appear to have properties they never defined.

The vulnerable shape is often a deep merge that trusts keys:

function merge(target, source) {
  for (const key in source) {
    const value = source[key];

    if (value && typeof value === "object") {
      target[key] ??= {};
      merge(target[key], value);
    } else {
      target[key] = value;
    }
  }

  return target;
}

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

merge({}, payload);

console.log({}.isAdmin);
// true in vulnerable merge patterns

The defense is boring and necessary:

  • Reject dangerous keys such as __proto__, prototype, and constructor when merging untrusted data.
  • Prefer Object.create(null) for dictionary targets.
  • Use well-maintained merge utilities that handle prototype-pollution cases.
  • Avoid writing to global prototypes in application code.

Prototype pollution is not an inheritance curiosity. It is a real security class caused by forgetting that prototypes are shared.

Key Takeaways

  • Property lookup checks the object first, then walks the prototype chain.
  • Object.getPrototypeOf() shows the internal prototype link.
  • Object.create(proto) creates an object with a chosen prototype.
  • Constructor .prototype becomes the internal prototype of instances created with new.
  • class and extends still use prototype mechanics.
  • Own properties shadow prototype properties.
  • Prototype pollution happens when untrusted keys modify shared prototypes.
Share this article:
javascriptintermediateadvancedobjectsfunctions