Skip to content
academia.sh

Lesson 01 / 17

Object Prototype

The prototype link every object carries, the order in which properties are searched through this link, the distinction between an own property and an inherited property, and where the chain ends.

Contents

The JavaScript Fundamentals course introduced the object as a key–value mapping: properties are written, read, deleted. This definition is enough for everyday use, but it leaves one question unanswered. Create an empty object and read its toString property and you get not undefined but a function. No one wrote that function there.

The answer stands at the center of the language’s object model: every object carries, in addition to its own properties, a link to another object. If the property you are looking for is not on the object itself, the search continues through this link. This lesson builds that link; the rest of the course — including class syntax — sits on top of it.

Two Copies of the Same Behavior

Throughout this course we will work with a single concrete example: a measurement record read from a sensor. A record carries three pieces of data (sensor id, measured value, moment of measurement) and offers at least one behavior (printing itself in readable form). The most direct setup is to write each record on its own as an object literal.

const recordOne = {
  sensor: "S-01",
  value: 21.4,
  time: 1000,
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
};

const recordTwo = {
  sensor: "S-02",
  value: 19.8,
  time: 1060,
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
};

console.log(recordOne.format());
console.log(recordTwo.format());
console.log(recordOne.format === recordTwo.format);
S-01@1000: 21.4
S-02@1060: 19.8
false

The last line makes the problem visible. Both records offer the same behavior, but in memory there are two separate function objects; the identity comparison says so. A thousand measurements produce a thousand copies, and when the formatting rule changes, it has to change in a thousand places.

The Introduction to Object-Oriented Programming lesson in the Programming Fundamentals course answered this problem with the class concept: behavior is written once in the class, objects share it. JavaScript’s answer sits one level lower and requires no class.

Every object, the moment it is created, is bound to another object — or to the value null. This link is called the prototype. The link is not an ordinary property of the object; in the language’s specification it appears as an internal field named [[Prototype]], and it is read with the Object.getPrototypeOf function.

Object.create sets up the link directly: it produces a new object whose prototype is the given object.

const measurementBehavior = {
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
  exceedsThreshold(threshold) {
    return this.value > threshold;
  },
};

function measurementRecord(sensor, value, time) {
  const record = Object.create(measurementBehavior);
  record.sensor = sensor;
  record.value = value;
  record.time = time;
  return record;
}

const recordOne = measurementRecord("S-01", 21.4, 1000);
const recordTwo = measurementRecord("S-02", 19.8, 1060);

console.log(recordOne.format());
console.log(recordTwo.exceedsThreshold(20));
console.log(recordOne.format === recordTwo.format);
console.log(Object.getPrototypeOf(recordOne) === measurementBehavior);
S-01@1000: 21.4
false
true
true

The third line now gives true: both records use the same function object, because the function stands not in the records but in the shared prototype. The fourth line tests the link itself — every claim about the object model throughout this lesson will be shown this way, with a directly testable comparison.

Data is separate in each record, behavior is in one place. This split is the entire point of the prototype model: the part that changes stands in the object, the part that does not stands in the chain.

Property Lookup Order

When you read a property, the runtime follows this order:

  1. Search among the object’s own properties. If found, return the value.
  2. If not found, move to its prototype and do the same search there.
  3. Continue until the chain reaches null. If found nowhere, return undefined.

Because the search stops at the first match, an object’s own property shadows a prototype property of the same name. The Scope and Lifetime lesson in the Programming Foundations course described an inner scope shadowing a name in an outer scope; the mechanism here is the same idea’s counterpart for objects.

const measurementBehavior = {
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
};

const record = Object.create(measurementBehavior);
record.sensor = "S-01";
record.value = 21.4;
record.time = 1000;

console.log(record.format());

record.format = function () {
  return `[raw] ${this.value}`;
};

console.log(record.format());
console.log(Object.hasOwn(record, "format"));
console.log(measurementBehavior.format.call(record));

delete record.format;
console.log(record.format());
S-01@1000: 21.4
[raw] 21.4
true
S-01@1000: 21.4
S-01@1000: 21.4

Three observations matter. First, assignment does not change the prototype’s function; it creates a new property belonging to the record. A write never climbs the chain, it always writes to the object itself. Second, the shadowed property does not disappear; it can still be called with call (the details of this operation belong to the Context and Closures topic). Third, when the own property is deleted, the chain becomes visible again — deletion looks as if it brought back the prototype’s definition, but it actually only removed the shadow.

Own Property and Inherited Property

Properties coming through the chain are not distinguished from own properties in most read operations; the places the distinction matters are counting and serialization.

const measurementBehavior = {
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
};

const record = Object.create(measurementBehavior);
record.sensor = "S-01";
record.value = 21.4;
record.time = 1000;

console.log("format" in record);
console.log(Object.hasOwn(record, "format"));
console.log(Object.keys(record).join(","));

const forInKeys = [];
for (const key in record) {
  forInKeys.push(key);
}
console.log(forInKeys.join(","));

console.log(JSON.stringify(record));
true
false
sensor,value,time
sensor,value,time,format
{"sensor":"S-01","value":21.4,"time":1000}

The in operator looks at the whole chain, Object.hasOwn only at the object itself. Object.keys is limited to own properties; the for...in loop also walks properties coming from the chain, which is why it is not a reliable tool for iterating over an object. JSON.stringify also works with own properties only — serialization takes only the data, leaves the behavior behind. This behavior gives an early hint of the distinction at the end of the lesson: an object’s data is in itself, its identity is in its chain.

The format name appearing in the for...in output here is because the prototype property is enumerable. How enumerability is controlled is the subject of the Property Descriptors lesson; class syntax’s difference on this point will be shown there too.

The End of the Chain

The chain is not infinite. Every object created with an object literal has Object.prototype as its prototype, and that object’s prototype is null. This is why names like toString, valueOf, and hasOwnProperty are found on every object.

const measurementBehavior = {
  format() {
    return `${this.sensor}@${this.time}: ${this.value}`;
  },
};
const record = Object.create(measurementBehavior);

console.log(Object.getPrototypeOf(record) === measurementBehavior);
console.log(Object.getPrototypeOf(measurementBehavior) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));

console.log(typeof record.toString);
console.log(typeof record.unknownProperty);

const dictionary = Object.create(null);
dictionary.sensor = "S-01";
console.log(Object.getPrototypeOf(dictionary));
console.log(typeof dictionary.toString);
console.log("toString" in dictionary);
true
true
null
function
undefined
null
undefined
false

The chain has three links: recordmeasurementBehaviorObject.prototypenull. When a nonexistent property is requested, the runtime walks all three stops and returns undefined; it does not throw an error. Search cost grows with chain length, but the work done at each stop is a dictionary lookup — exactly the average constant-cost access examined in the Hash Tables lesson of the Data Structures course.

An object produced with Object.create(null) has no chain at all. Such an object behaves as a pure dictionary: because inherited names like toString are not present, the risk of externally supplied keys colliding with built-in names is eliminated. In mappings whose keys come from outside the program, this setup also makes for...in and in results predictable. The language’s real counterpart for this same need is the Map type, taken up in the Advanced Collections topic.

Summary

  • Every object carries, in addition to its own properties, a link to a prototype object; the link is read with Object.getPrototypeOf and can be tested directly.
  • Property lookup starts from the object itself, moves up the chain if not found, and ends in undefined at the null stop.
  • Writing does not climb the chain; it always writes to the object itself and shadows a prototype property of the same name.
  • in looks at the whole chain; Object.hasOwn and Object.keys look only at own properties; JSON.stringify also works with own properties only.
  • The chain built by object literals reaches null through Object.prototype; Object.create(null) produces a prototype-less object that behaves as a pure dictionary.

Next Step

In this lesson the prototype was set up by hand: a shared behavior object was written first, then each record was bound to it with Object.create. This pattern works, but it does not resemble a type definition — deriving a new record type, filling in shared fields, and sharing behavior between two record types all require a repeating setup. The next lesson examines the built-in patterns for object-to-object inheritance and shows how the new operator sets up this link in your place.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close