Lesson 02 / 17
Prototypal Inheritance
The link constructor functions and the new operator set up, extending the prototype chain to two layers, type-query operations, and the link changing at runtime.
Contents
The previous lesson set up the prototype link by hand: shared behavior was gathered in
one object, each record was bound to it with Object.create, and fields were filled in
one by one. The pattern works, but it has two gaps. Filling in fields and binding
behavior sit in separate places, and there is no built-in setup for deriving one record
type from another.
The language offers a built-in path for both. This lesson opens that path — the
constructor function and the new operator — then writes by hand what new does, to
show that it is not magic but a shorthand for the three steps you saw in the previous
lesson.
The Constructor Function
Every function in JavaScript carries an ordinary property named prototype. This
property is not the function’s own prototype; it is the object that will become the
prototype of the objects the function produces when called with new. Because the name
similarity hides this distinction, the two concepts must not be confused.
A function written to be called with new is called a constructor function. It
fills in fields through this, while behaviors are written to the prototype object.
function MeasurementRecord(sensor, value, time) { this.sensor = sensor; this.value = value; this.time = time; } MeasurementRecord.prototype.format = function () { return `${this.sensor}@${this.time}: ${this.value}`; }; MeasurementRecord.prototype.exceedsThreshold = function (threshold) { return this.value > threshold; }; const recordOne = new MeasurementRecord("S-01", 21.4, 1000); const recordTwo = new MeasurementRecord("S-02", 19.8, 1060); console.log(recordOne.format()); console.log(Object.getPrototypeOf(recordOne) === MeasurementRecord.prototype); console.log(recordOne.format === recordTwo.format); console.log(Object.keys(recordOne).join(",")); console.log(recordOne.constructor === MeasurementRecord); console.log(Object.hasOwn(MeasurementRecord.prototype, "constructor"));
S-01@1000: 21.4 true true sensor,value,time true true
The result is the same as the previous lesson’s setup: data separate in each record, behavior in one place. The fourth line confirms this — the record’s own properties are only the three data fields.
The fifth and sixth lines expose a detail. Every function’s prototype object, the
moment it is created, carries a property named constructor, and this property points
at the function itself. The expression recordOne.constructor is not found on the
record; it comes from the chain, through MeasurementRecord.prototype. So constructor
is not an object’s “true type,” it is an ordinary property encountered on its chain — the
cost of this distinction will be seen in the next section.
What the new Operator Does
The expression new MeasurementRecord(...) breaks down into four steps:
- An empty object is created whose prototype is
MeasurementRecord.prototype. - The constructor function is called with
thisbound to this object. - If the constructor function returns an object, that object is the result; if not, the object from the first step is the result.
- The result becomes the value of the expression.
All of these steps are ordinary operations; none of them carries language-specific privilege. A function doing the same job can be written.
function MeasurementRecord(sensor, value, time) { this.sensor = sensor; this.value = value; this.time = time; } MeasurementRecord.prototype.format = function () { return `${this.sensor}@${this.time}: ${this.value}`; }; function newInstance(Constructor, ...args) { const obj = Object.create(Constructor.prototype); const result = Constructor.apply(obj, args); return typeof result === "object" && result !== null ? result : obj; } const viaHelper = newInstance(MeasurementRecord, "S-01", 21.4, 1000); const viaOperator = new MeasurementRecord("S-01", 21.4, 1000); console.log(viaHelper.format()); console.log(Object.getPrototypeOf(viaHelper) === MeasurementRecord.prototype); console.log(JSON.stringify(viaHelper) === JSON.stringify(viaOperator)); console.log(viaHelper instanceof MeasurementRecord);
S-01@1000: 21.4 true true true
The newInstance function stands in for the new operator and produces an
indistinguishable result. The apply call explicitly sets up the this binding; this
operation’s rules will be covered in the first two lessons of the Context and Closures
topic. The real observation here is this: new is a shorthand laid on top of the
prototype model, not a separate mechanism.
The newInstance function we wrote does not cover the whole of new: constructors
defined with class syntax cannot be called without new, and details like new.target
fall outside this shorthand. What matters for the model is the three lines above.
Extending the Chain
Consider a subtype of measurement records: a calibrated measurement carrying a sensor’s systematic drift. Its fields cover the measurement record’s fields and add an offset value on top; how the read value is computed differs, how it is formatted is the same.
Extending the chain to two layers requires two jobs: the subtype’s prototype has to be bound to the supertype’s prototype, and while the sub-constructor runs, the super-constructor’s field-filling job has to run too.
function MeasurementRecord(sensor, value, time) { this.sensor = sensor; this.value = value; this.time = time; } MeasurementRecord.prototype.format = function () { return `${this.sensor}@${this.time}: ${this.value}`; }; MeasurementRecord.prototype.reading = function () { return this.value; }; function CalibratedMeasurement(sensor, value, time, offset) { MeasurementRecord.call(this, sensor, value, time); this.offset = offset; } CalibratedMeasurement.prototype = Object.create(MeasurementRecord.prototype); CalibratedMeasurement.prototype.constructor = CalibratedMeasurement; CalibratedMeasurement.prototype.reading = function () { return this.value + this.offset; }; const calibrated = new CalibratedMeasurement("S-03", 21.4, 1120, -0.4); console.log(calibrated.format()); console.log(calibrated.reading()); console.log(MeasurementRecord.prototype.reading.call(calibrated)); console.log(Object.getPrototypeOf(calibrated) === CalibratedMeasurement.prototype); console.log(Object.getPrototypeOf(CalibratedMeasurement.prototype) === MeasurementRecord.prototype); console.log(calibrated.constructor === CalibratedMeasurement);
S-03@1120: 21.4 21 21.4 true true true
The chain now has four links: calibrated → CalibratedMeasurement.prototype →
MeasurementRecord.prototype → Object.prototype → null. format is not found on the
second link, it is found on the third; reading is found on the second link, so it
shadows the third’s definition. The polymorphism introduced in the Introduction to
Object-Oriented Programming lesson of the Programming Fundamentals course is exactly this
shadowing here: the same name corresponds to different implementations at different
layers of the chain, and lookup order decides which one runs.
Three lines deserve attention. MeasurementRecord.call(this, ...) runs the
super-constructor’s field-filling job on the new object. If the link set up with
Object.create(MeasurementRecord.prototype) had instead been set up as
CalibratedMeasurement.prototype = MeasurementRecord.prototype, the two types would
share the exact same prototype object, and every behavior added to the subtype would leak
into the supertype too. Writing the constructor property back by hand is the
consequence of the first section’s observation: when the prototype object is replaced,
constructor is lost along with it, and if it is not rewritten, calibrated.constructor
finds MeasurementRecord from the chain.
Type Querying on the Chain
The instanceof operator asks whether the function on its right’s prototype object is
found on the chain of the object on its left. This query, which looks like class
membership, is actually a chain scan.
function MeasurementRecord(sensor, value) { this.sensor = sensor; this.value = value; } function CalibratedMeasurement(sensor, value, offset) { MeasurementRecord.call(this, sensor, value); this.offset = offset; } CalibratedMeasurement.prototype = Object.create(MeasurementRecord.prototype); CalibratedMeasurement.prototype.constructor = CalibratedMeasurement; const calibrated = new CalibratedMeasurement("S-03", 21.4, -0.4); const plain = new MeasurementRecord("S-01", 21.4); console.log(calibrated instanceof CalibratedMeasurement); console.log(calibrated instanceof MeasurementRecord); console.log(calibrated instanceof Object); console.log(plain instanceof CalibratedMeasurement); console.log(MeasurementRecord.prototype.isPrototypeOf(calibrated)); console.log(CalibratedMeasurement.prototype.isPrototypeOf(plain)); let stop = calibrated; const chain = []; while (stop !== null) { stop = Object.getPrototypeOf(stop); chain.push(stop === null ? "null" : stop.constructor.name); } console.log(chain.join(" -> "));
true true true false true false CalibratedMeasurement -> MeasurementRecord -> Object -> null
The last block walks the chain from start to end and prints it, making visible where the
instanceof results come from. Because all three prototype objects are found on the
calibrated measurement’s chain, all three queries answer positively; because the plain
record does not carry the subtype’s prototype on its chain, the fourth query is negative.
isPrototypeOf asks the same question from the reverse direction and looks not at the
function but directly at the prototype object.
The Liveness of the Link
The prototype link is not a copy, it is a live reference. A property added to the prototype after an object is created immediately becomes visible on every existing object.
function MeasurementRecord(sensor, value) { this.sensor = sensor; this.value = value; } MeasurementRecord.prototype.format = function () { return `${this.sensor}: ${this.value}`; }; const record = new MeasurementRecord("S-01", 21.4); console.log(record.format()); console.log(typeof record.summarize); MeasurementRecord.prototype.summarize = function () { return `${this.sensor} record`; }; console.log(typeof record.summarize); console.log(record.summarize()); const otherBehavior = { format() { return `[${this.sensor}] ${this.value}`; }, }; Object.setPrototypeOf(record, otherBehavior); console.log(record.format()); console.log(record instanceof MeasurementRecord); console.log(typeof record.summarize);
S-01: 21.4 undefined function S-01 record [S-01] 21.4 false undefined
summarize was added after the record was created and becomes accessible with no
re-creation at all; because the search is done fresh on every read, the link is live.
Object.setPrototypeOf changes an existing object’s link. The consequences are broad:
the record no longer sees the supertype’s behaviors, the instanceof result flips, and
because runtimes optimize based on object shapes, access performance drops. For this
reason the link is set up at the moment the object is created and is not changed
afterward; Object.create or new exist for exactly this.
Summary
- Every function’s
prototypeproperty is the object that will become the prototype of the objects it produces withnew; it is not the function’s own prototype. newcreates an empty object whose prototype is bound to theprototypeobject, runs the constructor on this object, and returns the result; the same job can be written by hand withObject.create.- Extending the chain consists of two jobs: binding the sub-prototype to the super-prototype, and calling the super-constructor from inside the sub-constructor.
- A same-named definition at a lower layer shadows the one above it; polymorphism is the result of this lookup order.
instanceofandisPrototypeOfask not about type membership but whether a prototype object is found on the chain.- The link is live: properties added to the prototype afterward become visible on existing objects.
Next Step
In this lesson the chain was set up by hand, and every step of the setup was made
visible: the prototype assignment, the constructor repair, the super-constructor
call. All three steps being easy to forget is the reason a syntax was added to the
language that gathers them into a single declaration. The next lesson introduces class
syntax and shows, with the same tests, which operation from this lesson each of its lines
corresponds to.
To keep your progress and take notes, Log in
My notes
Log in to take notes.