Introduction

JavaScript allows an object to use properties and methods that are not stored directly inside it. For example, an object can call a method that is defined on another object connected to it. This behaviour is provided by prototypes.

The main purpose of this article is to explain how JavaScript searches for a property and how that search is related to constructors and classes. The most important point is that JavaScript does not immediately search every object in the program. It starts with the object being used, checks its own properties, and then follows a chain of prototype links until it finds the requested property or reaches the end of the chain.

Two terms are often confused when learning this topic. Every ordinary object has an internal [[Prototype]] link, while most regular functions have a public property named prototype. These are connected, but they are not the same thing. Understanding this difference makes inheritance and class-based code easier to follow.

The two meanings that are easy to mix up

An object's [[Prototype]] is an internal link to another object or to null. JavaScript code does not access the internal slot by writing object.[[Prototype]]; that notation belongs to the language specification. Use Object.getPrototypeOf() when you need to inspect it:

const record = { id: 42 };

console.log(Object.getPrototypeOf(record) === Object.prototype);
// true

By contrast, prototype is an ordinary public property found on constructable functions:

function Item(code) {
  this.code = code;
}

const item = new Item("A100");

console.log(Object.getPrototypeOf(item) === Item.prototype);
// true

The important relationship is:

item.[[Prototype]]  →  Item.prototype

The left side describes an internal link on item. The right side is a normal property on the function object Item. Object.getPrototypeOf(item) is how we observe the left side in code.

The older __proto__ accessor appears in legacy code and browser consoles, but it should normally not be used in new code. Object.getPrototypeOf() explains the purpose more clearly and is the standard way to inspect an object's prototype.

Property lookup is a chain search

Property lookup can be understood as a step-by-step search. JavaScript first checks whether the object has the requested property itself. If it does not, JavaScript checks the object's prototype. The same process continues until the property is found or the next prototype is null.

Prototype lookup from an instance through its constructor prototype and Object.prototype to null

For a typical object created with a constructor, the chain contains the following parts:

  1. the instance;
  2. Constructor.prototype;
  3. Object.prototype; and
  4. null, which ends the search.

An own property is stored directly on the object being examined. An inherited property is found farther up its prototype chain. Object.hasOwn() lets us distinguish them:

function Item(code) {
  this.code = code;
}

Item.prototype.describe = function () {
  return `Item ${this.code}`;
};

const item = new Item("A100");

console.log(Object.hasOwn(item, "code"));     // true
console.log(Object.hasOwn(item, "describe")); // false
console.log(item.describe());                  // "Item A100"

In this example, item owns code, but it can still use describe because the function is found on Item.prototype. This is the reason prototype methods can be shared by several objects without placing a separate copy of the function inside every object.

Following one missing property through several links

The following example is deliberately small enough to run in a browser console or Node.js. It creates a longer chain without using __proto__:

const base = {
  category: "general",
  label() {
    return `${this.name} (${this.category})`;
  },
};

const specialised = Object.create(base);
specialised.category = "hardware";

const product = Object.create(specialised);
product.name = "Keyboard";

console.log(product.label());    // "Keyboard (hardware)"
console.log(product.missing);    // undefined

The example can be run in a browser console or with Node.js. It demonstrates that a property can be found several links away from the object being used.

For product.label, lookup proceeds like this:

  1. product does not own label.
  2. specialised does not own label.
  3. base owns label, so the search stops and that function is called.

Inside the inherited method, this is product because the call is written as product.label(). Therefore, this.name finds the own property product.name. Lookup for this.category starts on product, finds nothing there, and then finds specialised.category.

For product.missing, the name is absent from product, specialised, base, and Object.prototype. The next prototype is null, so the lookup finishes and the property access evaluates to undefined.

The undefined result does not prove that the object has a property with an undefined value. It may mean that the property was not found at all. Therefore, Object.hasOwn(product, "missing") should be used when the purpose is to check whether the property belongs directly to product.

Shadowing stops the search early

If an object and one of its prototypes use the same property name, the object's own property wins because it is encountered first. This is property shadowing:

console.log(product.category); // "hardware"

product.category = "accessories";

console.log(product.category);    // "accessories"
console.log(specialised.category); // "hardware"

Assigning product.category creates or updates an own property on product; it does not overwrite the property on specialised. Deleting the own property would reveal the inherited value again:

delete product.category;
console.log(product.category); // "hardware"

The important rule is that JavaScript uses the first matching property found while moving from the object towards null. This is the property lookup rule behind both inheritance and shadowing. If product owns category, the search stops at product. If it does not, the search continues to the next prototype.

What new connects

When JavaScript evaluates an expression such as new Item("A100"), several operations take place:

  1. It creates a new object.
  2. It sets that object's [[Prototype]] to the object currently held in Item.prototype.
  3. It calls Item with the new object as this.
  4. It normally returns the new object. A constructor that explicitly returns another object can replace that result.

This is why instance data is commonly assigned in the constructor while methods that can be shared are placed on the constructor's prototype object:

function Item(code) {
  this.code = code;
}

Item.prototype.describe = function () {
  return `Item ${this.code}`;
};

const first = new Item("A100");
const second = new Item("B200");

console.log(first.describe === second.describe); // true

Both property lookups reach the same function object on Item.prototype.

Compare that with creating a function inside the constructor:

function ItemWithOwnMethod(code) {
  this.code = code;
  this.describe = function () {
    return `Item ${this.code}`;
  };
}

const first = new ItemWithOwnMethod("A100");
const second = new ItemWithOwnMethod("B200");

console.log(first.describe === second.describe); // false

In this version, every constructor call creates a new function and stores it directly on the new object. This can be useful when each instance needs a separate closure or separate behaviour. However, when all instances use the same method, placing the method on the prototype is usually more suitable because the method can be shared.

How class fits the model

JavaScript class syntax still creates prototype relationships. An instance method declared in a class body is installed on the class's prototype object:

class Item {
  constructor(code) {
    this.code = code;
  }

  describe() {
    return `Item ${this.code}`;
  }
}

const item = new Item("A100");

console.log(Object.getPrototypeOf(item) === Item.prototype); // true
console.log(Object.hasOwn(Item.prototype, "describe"));      // true
console.log(Object.hasOwn(item, "describe"));                // false

It is common to hear that classes are only “syntactic sugar” for constructor functions. This is not a complete explanation. Classes use the prototype system, but they also have their own rules. For example, class bodies run in strict mode, class constructors require new, and class methods are not enumerable in the same way as ordinary object properties. Features such as extends, super, static members, private members, and fields also have defined behaviour. Therefore, classes should be understood as a structured feature that uses prototypes, not simply as a different spelling of every constructor function.

Why modifying built-in prototypes is risky

JavaScript technically allows code such as Array.prototype.someNewMethod = .... However, application code should normally avoid changing built-in prototypes. A future version of JavaScript or a library could use the same name for a different purpose. Additionally, unrelated code can observe the new property because the change affects every matching array in the same JavaScript environment.

Polyfills are a specialised exception. A polyfill provides a standard feature when an older environment does not support it, so it must follow the expected standard behaviour carefully. This is different from adding an application-specific convenience method to a built-in prototype.

From prototypes to data structures

This mental model carries into later work with linked lists and trees. A linked-list node's next property is an ordinary reference to another node, while a node created from a class or constructor can obtain its shared methods through a prototype. Tree nodes similarly use object references for their structure and may use class syntax for construction and shared behaviour.

These are separate relationships. next, left, and right are data-structure links created by the programmer, while [[Prototype]] is the language-level link used during property lookup. Keeping the two ideas separate makes both types of code easier to understand. A linked list may use a next reference, but that does not mean that the list is using a prototype chain to connect its nodes.

Key takeaways

  • An object's internal [[Prototype]] and a function's public prototype property are different things.
  • Object.getPrototypeOf(value) is the standard inspection tool; avoid the legacy __proto__ accessor in new code.
  • Property lookup checks own properties first, then follows prototypes until it finds a match or reaches null.
  • An own property shadows an inherited property with the same key.
  • new Constructor() normally links the new object's [[Prototype]] to Constructor.prototype.
  • Prototype methods can be shared by many instances, whereas a function created inside a constructor is recreated for each instance.
  • Class instances still use prototype lookup, but class syntax also adds meaningful language rules and features.
  • Data-structure links such as next or left are not prototype links.

Sources