Lesson 06 / 17
this Binding
Context determined at the call site; the default, implicit, explicit, and constructor binding rules, their priority order, and a method detaching from its object.
Contents
Throughout the Prototype Model topic, methods worked through this, and this gave the
impression that a function somehow knows the object it was defined on. The impression is
wrong. As in the previous lesson’s Object.assign(copy, this, ...) call, a function
sitting in a prototype chain can operate on an object it never appeared in at all.
The rule is this: the value of this is determined not by where the function is
defined, but by how it is called. The same function binds to four different
values under four different call forms. This lesson tells the four apart and builds the
priority order among them.
Default Binding
The plainest call form is calling a function with no object attached to it at all. In
strict mode, the this value is undefined.
function readBinding() { return this; } console.log(readBinding() === undefined); console.log(typeof globalThis); const nonStrictReadBinding = new Function("return this;"); console.log(nonStrictReadBinding() === globalThis); console.log(nonStrictReadBinding() === undefined);
true object true false
In non-strict mode, the same call binds this to the global object; globalThis is
this object’s standard name. The difference matters: a method called by accident
without its object throws an error immediately in strict mode, while in non-strict mode
it silently writes a property onto the global object. Because module files and class
bodies run in strict mode, the first is the behavior that applies throughout the
measurement record examples.
Implicit Binding
If a function is called through a property of an object, this binds to that object.
What decides this is the object to the left of the dot operator at the call site.
const record = { sensor: "S-01", value: 21.4, time: 1000, format() { return `${this.sensor}@${this.time}: ${this.value}`; }, }; console.log(record.format()); const brokenFormat = record.format; try { console.log(brokenFormat()); } catch (error) { console.log(`broken call: ${error.constructor.name}`); } const records = [record]; try { console.log(records.map(record.format)[0]); } catch (error) { console.log(`callback: ${error.constructor.name}`); } console.log(records.map((r) => r.format())[0]); const measurement = { sensor: "S-OUT", source: { sensor: "S-IN", format() { return `bind: ${this.sensor}`; }, }, }; console.log(measurement.source.format());
S-01@1000: 21.4 broken call: TypeError callback: TypeError S-01@1000: 21.4 bind: S-IN
The second and third lines show the language’s most commonly hit trap. The expression
record.format gives a function value; the bond with the object is not carried
inside this value. Once the value is assigned to a variable or passed as a callback,
the call site changes and binding falls back to the default rule. Because this is
undefined in strict mode, reading this.sensor throws an error.
This is a consequence of the observation in the Programming Fundamentals course’s Value and Reference, and Passing lesson: what is passed is the function itself, not the context it sits in. The fix on the fourth line is the most direct one — writing a wrapper that makes the call through the object.
The last line shows that chain length plays no role: only the last link counts. In
the call measurement.source.format(), this is the source object; the outer
measurement is never considered at all.
Explicit Binding
The call, apply, and bind operations state the context explicitly at the call
site. Their details are the next lesson’s subject; here, only the fact that this rule
is a third form is shown.
const measurementBehavior = { format() { return `${this.sensor}@${this.time}: ${this.value}`; }, }; const chained = Object.create(measurementBehavior); chained.sensor = "S-01"; chained.value = 21.4; chained.time = 1000; const foreign = { sensor: "S-99", value: 5.5, time: 7 }; console.log(chained.format()); console.log(measurementBehavior.format.call(foreign)); console.log(chained.format.call(foreign)); console.log(Object.getPrototypeOf(foreign) === measurementBehavior);
S-01@1000: 21.4 S-99@7: 5.5 S-99@7: 5.5 false
The last line confirms this lesson’s opening claim: the foreign object’s chain
carries no measurement behavior, yet the same function works on its data without
trouble. The prototype chain determines where the function is found; this
determines which data it works on. The two are independent of each other.
Constructor Binding
The new operator binds this to the newly created object, in the second of the
steps ordered in the second lesson.
function MeasurementRecord(sensor, value) { this.sensor = sensor; this.value = value; } MeasurementRecord.prototype.format = function () { return `${this.sensor}: ${this.value}`; }; const viaConstructor = new MeasurementRecord("S-01", 21.4); console.log(viaConstructor.format()); console.log(Object.getPrototypeOf(viaConstructor) === MeasurementRecord.prototype); try { MeasurementRecord("S-02", 19.8); } catch (error) { console.log(`without new: ${error.constructor.name}`); } class ClassRecord { constructor(sensor) { this.sensor = sensor; } format() { return `class: ${this.sensor}`; } } const instance = new ClassRecord("S-03"); console.log(instance.format()); const broken = instance.format; try { broken(); } catch (error) { console.log(`class method broken: ${error.constructor.name}`); }
S-01: 21.4 true without new: TypeError class: S-03 class method broken: TypeError
The third line shows how calling a constructor function without new turns out in
strict mode: because this is undefined, the first assignment throws an error. Class
syntax forbidding this call outright exists precisely to prevent this silent failure.
The last line repeats an important point: class syntax does not solve the detachment problem. Class methods are also plain functions sitting on the prototype, and they lose their binding the same way once separated from their object.
Losing Context in a Callback
The four rules also explain where errors come from. When a method passes a function into another function inside its body, that function becomes a separate call; the outer method’s context does not carry over to it.
const collector = { unit: "C", measurements: [21.4, 22.8, 20.1], formattedList() { return this.measurements.map(function (value) { return `${value}${this.unit}`; }); }, formattedListThisArg() { return this.measurements.map(function (value) { return `${value}${this.unit}`; }, this); }, }; try { console.log(collector.formattedList()); } catch (error) { console.log("in callback:", error.constructor.name, "-", error.message); } console.log(collector.formattedListThisArg());
in callback: TypeError - Cannot read properties of undefined (reading 'unit') [ '21.4C', '22.8C', '20.1C' ]
In the first call, the callback function falls to default binding; because this is
undefined in strict mode, reading the property throws an error. Some array methods
take a second parameter that binds the callback to a given object; the second call
uses it.
This parameter is not present in every interface. The general fix is one of two steps:
fixing context with explicit binding (next lesson) or using a function form that
establishes no this binding of its own (third lesson). The root of the problem is
that the call site is independent of where the function was defined.
Priority Order
When more than one rule looks like it applies at once, which one wins is fixed: constructor binding overrides explicit binding; explicit binding overrides implicit binding; implicit binding overrides default binding.
function writeSensor(sensor) { this.sensor = sensor; return this; } const target = { sensor: "start" }; const otherTarget = { sensor: "other" }; const bound = writeSensor.bind(target); bound("explicit-bound"); console.log(target.sensor); otherTarget.run = bound; otherTarget.run("implicit-attempt"); console.log(target.sensor); console.log(otherTarget.sensor); const produced = new bound("constructor"); console.log(produced.sensor); console.log(target.sensor); console.log(Object.getPrototypeOf(produced) === writeSensor.prototype); const obj = { sensor: "implicit", read() { return this.sensor; }, }; console.log(obj.read()); console.log(obj.read.call(otherTarget));
explicit-bound implicit-attempt other constructor implicit-attempt true implicit other
The second and third lines say explicit binding beats implicit binding: even though the
bound function is called as a property of an object, the write lands on target,
otherTarget stays unchanged. The fourth and fifth lines show constructor binding
beating explicit binding: called with new, the bound target is ignored, a new object
is used, and the chain is set up to the constructor’s prototype object.
The order can be summarized in one sentence: new > bind/call/apply > dot
operator > none. There is a fifth case — functions that fit none of the rules and
have no this value of their own — and it will be covered two lessons from now.
Summary
- The
thisvalue is determined by looking at how a function is called, not where it is defined. - In default binding,
thisisundefinedin strict mode and the global object in non-strict mode. - In implicit binding, the object to the left of the dot operator is bound; only the last link counts.
- A method’s bond breaks when it is assigned to a variable or passed as a callback; class methods are not exempt from this behavior.
- The prototype chain determines where a function will be found,
thisdetermines which data it operates on; the two are independent. - Priority order: constructor binding, explicit binding, implicit binding, default binding.
Next Step
In this lesson, explicit binding was introduced only as a rule; the differences among
the three operations that establish it were not covered. How do call and apply pass
arguments differently, why does bind produce a permanent bond, and how do these
operations lend a method not in an object’s chain to that object? The next lesson
examines these three operations and the function-borrowing pattern.
To keep your progress and take notes, Log in
My notes
Log in to take notes.