DevPath · Learn to code ESPTEN

Object-oriented programming

Dynamic this: bind, call and apply

this depends on HOW it is called

The most important rule about this: it does not matter where you defined the function, it matters how you invoke it. Think of this as an empty seat filled in at call time, based on whatever is "to the left of the dot".

const person = {
  name: "Ana",
  greet() {
    return "Hi, I'm " + this.name;
  },
};
person.greet();          // this = person  ->  "Hi, I'm Ana"
const loose = person.greet;
loose();                 // this = undefined ->  error or "undefined"!

When you store the method in loose and call it "bare", there is no longer anything to the left of the dot: this is lost. This is the dreaded "loss of this".

Where it bites you in practice

It happens whenever you pass a method as a callback and other code calls it for you:

setTimeout(person.greet, 100);     // this is lost
button.addEventListener("click", obj.handle); // this is lost
const { greet } = person;          // destructuring also detaches it

bind: pins this and returns a new function

bind does not call the function: it creates a bound copy tied to a fixed this that never changes again, even if you invoke it bare.

const greetAna = person.greet.bind(person);
greetAna();                // "Hi, I'm Ana"  (even when called bare)
setTimeout(greetAna, 100); // now it works

call and apply: invoke NOW while pinning this

Unlike bind, these run the function immediately while indicating which this to use. The only difference between them is how you pass the arguments:

function introduce(greeting, mark) {
  return greeting + ", I'm " + this.name + mark;
}
const ana = { name: "Ana" };
introduce.call(ana, "Hi", "!");      // call: loose args
introduce.apply(ana, ["Hi", "!"]);   // apply: args in array
// Both -> "Hi, I'm Ana!"

Trick to remember: apply uses an array.

Modern solution: class fields with arrow functions

Arrow functions do not have their own this: they inherit it from where they were created. If you define a method as a field with an arrow, it stays "welded" to the instance and never loses this, even if you pass it as a callback:

class Button {
  constructor(text) {
    this.text = text;
  }
  // class field with arrow: this will always be this instance
  click = () => {
    return "Pressed: " + this.text;
  };
}
const b = new Button("Accept");
const fn = b.click;
fn(); // "Pressed: Accept"  (this is not lost)

Examples

The loss of this and how bind fixes it

const counter = {
  value: 10,
  read() {
    return this.value;
  },
};
const loose = counter.read;
const bound = counter.read.bind(counter);
console.log("Bound:", bound());        // 10
console.log("Loose this:", loose === counter.read);

call vs apply: same function, different way to pass args

function range(min, max) {
  return this.name + " between " + min + " and " + max;
}
const sensor = { name: "Temperature" };
console.log(range.call(sensor, 0, 100));
console.log(range.apply(sensor, [0, 100]));

Arrow in a class field: this welded to the instance

class Timer {
  seconds = 0;
  tick = () => {
    this.seconds++;
    return this.seconds;
  };
}
const t = new Timer();
const fn = t.tick;     // we "detach" it
console.log(fn());     // 1  (still works)
console.log(fn());     // 2
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Inheritance and encapsulationStatic members and private methods →