Programming Languages · Objects and Modules

Prototype Chains and Delegation-Based Dispatch

How property lookup walks a prototype chain, why methods are stored once per prototype instead of once per object, and where the chain design costs lookup time.

Class-based dispatch fixes the method set at class definition time and stores the mapping from selector to implementation in a per-class table. A prototype-based system stores no such table. Each object holds a reference to another object, its prototype, and lookup follows that reference until it finds the property or reaches an object with no prototype. The mechanism is smaller than a class system and considerably more flexible, and the flexibility has a specific cost that appears whenever a program does the thing prototypes make easy: mutating a shared prototype at run time.

Lookup as a chain walk

The fundamental operation is property read. Given a receiver and a property key, the algorithm is:

  1. If the receiver has an own property with that key, return its value.
  2. Otherwise, if the receiver has a prototype, set the receiver to the prototype and repeat.
  3. Otherwise, the property is absent.

In ECMAScript terms the search is defined over an internal slot holding the prototype and an own-property table, and the loop is bounded by the length of the chain. Every ordinary object has a prototype except one, the root, whose prototype is the null value. A chain of length n therefore costs n own-property lookups in the worst case, and each of those lookups is itself a hash-table probe or an inline-slot comparison, depending on how the engine represents the object.

The root of the chain carries the shared library: Object.prototype in ECMAScript, with methods such as hasOwnProperty and toString. A newly created plain object from an object literal has no own properties, so a call to toString on it resolves one link up, at the root. This is not an implementation quirk; it is the definition, and engines specialise it heavily.

Writes behave differently from reads, and the asymmetry matters. Assignment to a property normally creates an own property on the receiver, shadowing anything with the same key higher in the chain. The prototype is not modified. The shadowing rule is what makes per-object state possible without copying the methods: the methods live once on the prototype, and each instance owns only its data fields. An object with three data fields and access to thirty methods occupies space for three fields and one prototype reference, not thirty-three fields. There is no visibility modifier anywhere in this arrangement, which is the point of comparison with what private fields actually hide: a property on a prototype is public to any code holding the object.

The exception to the shadowing rule is a property defined as an accessor with a setter. If the chain contains an accessor for the key, assignment invokes the setter with the original receiver as this rather than creating an own property. That behaviour is how prototype-level accessors implement computed properties across a family of objects, and it is also a source of surprise when a data property is later replaced by an accessor on the prototype: previously shadowed assignments start reaching the setter instead.

Methods are just function-valued properties

A method call in a prototype system is two operations: read the property, then call the result with the receiver bound to this. There is no separate method table and no selector-to-slot mapping. This has a consequence that class systems cannot express without reflection: a method can be replaced on a single object, on a shared prototype, or on the root, and the change takes effect immediately for every lookup that passes through the modified link.

Replacement on the receiver gives per-object behaviour. Replacement on a prototype gives behaviour for every object that delegates to it and does not shadow the key. Replacement on the root affects everything, including objects created before the assignment, because lookup is performed at call time rather than bound at creation.

Because dispatch is a property read, dispatch and field access share one code path. A caller cannot tell from the call site whether the callee is a method defined on a prototype or a function stored as an own property; both are the same operation. Languages built on this model therefore tend to treat functions as ordinary values, and the boundary between “method” and “callback” disappears. That uniformity is the design’s main expressive advantage and the reason its dispatch is discussed alongside function calls rather than separately from them.

Constructors are a convention, not a mechanism

The new operator in ECMAScript looks like class instantiation but decomposes into three steps that use nothing beyond the primitives above: create a new object whose prototype is the current value of the constructor function’s prototype property, call the constructor with that object as this, and return the object unless the constructor returned an object itself. A prototype property is created automatically on every function object, holding a plain object whose own prototype is Object.prototype. The constructor’s prototype property is an ordinary data property, so reassigning it changes the prototype of every subsequently constructed object, and leaves existing objects pointing at the old prototype.

function Counter(start) {
  this.count = start;            // own property, per instance
}
Counter.prototype.increment = function () {
  this.count += 1;               // 'this' is the receiver, resolved at call time
  return this.count;
};

const a = new Counter(0);
const b = new Counter(10);
// a and b share one function object; only 'count' is duplicated
console.log(Object.getPrototypeOf(a) === Counter.prototype); // true

Nothing in that code requires Counter to be a class. The class syntax introduced later in the language is a notation over the same primitives: methods declared in a class body become non-enumerable properties of the class’s prototype object, and extends sets up the prototype link between the two prototype objects as well as between the two constructor functions. The resulting arrangement has two separate links that both have to be established: an instance’s prototype is the derived prototype, and the derived prototype’s own prototype is the base prototype. Dropping the second link would leave instances of the derived class unable to find base methods.

Delegation also composes without inheritance hierarchies. An object can delegate to any object, so behaviour can be borrowed from several sources by copying references or by inserting links, and a chain can be built at run time for a single instance. The cost is that the resulting shape is a graph rather than a tree, and the language must define what happens when the graph contains a cycle. ECMAScript forbids it: setting a prototype that would create a cycle throws a TypeError, and the check walks the prospective chain, so a prototype assignment is O(chain length) rather than O(1).

What the chain costs an optimiser

The naive implementation is a pointer chase per link, and the language permits the chain to change at any moment, so a compiler cannot assume a fixed depth. The standard technique is an inline cache keyed on the receiver’s shape: the engine records, for a given property access, the shape of the object it saw and the offset at which the property was found, and checks the shape on the next execution. A hit costs a comparison and a load. A miss falls back to the general walk and updates the cache. Because the chain can be mutated, adding or removing a property invalidates the affected caches, and an engine that supports fast property access must be able to invalidate selectively rather than flushing everything. This is the same machinery described in inline caches and polymorphic call sites, applied to property reads instead of method dispatch.

A long chain is expensive in the worst case and cheap in the common one. A chain of depth d with a property found at the end costs d probes on a cold lookup; with an inline cache and a stable shape it costs one shape check.

Failure modes

Shadowing is the most common source of bugs. An assignment intended to update shared state creates an own property instead, and the prototype’s copy is left untouched. A misspelled key does the same thing silently, because a write never fails for a missing property; only reads return the undefined value. A sealed or frozen object converts the silent shadow into a throw or a no-op depending on strict mode, which is a reason to use those operations on shared prototypes.

Mutating Object.prototype affects every object in the program, including objects from libraries and from the host environment, and a name collision with a future standard method is not detectable in advance. This is why extending the root prototype is discouraged in library code and why the language’s own additions are defined with non-enumerable properties.

Enumeration has its own rule. for...in walks enumerable properties including inherited ones, while Object.keys returns own enumerable keys only. Code that assumes one behaviour and receives the other will iterate over methods it did not define.

Prototype pollution is the security-relevant version of the same property. An attacker who controls a key used in a merge or a deserialiser can write to a prototype instead of to the target object, and every object that delegates to that prototype acquires the injected property. The defence is to reject the special key names used to reach prototypes, and to create maps without a prototype link for untrusted keys.

The model also interacts badly with value semantics. Because objects are references and the prototype link is part of the object, copying an object by field is a shallow copy that retains the link, and a deep copy has to decide what to do with a chain that may be shared or may contain cycles. Class-based systems have the same problem in principle, but they at least have a class identity to compare against.

References

  1. ECMAScript Language Specification: Ordinary Object Internal Methods and Internal Slots — Ecma International
  2. Self: The Power of Simplicity — David Ungar and Randall B. Smith, OOPSLA 1987
  3. The Art of the Metaobject Protocol — Gregor Kiczales, Jim des Rivieres, Daniel G. Bobrow, MIT Press, 1991