Skip to content
academia.sh

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 this value is determined by looking at how a function is called, not where it is defined.
  • In default binding, this is undefined in 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, this determines 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.

Start typing to search.

↑↓ Esc navigate · open · close