this and binding

Work out what this is in any piece of code, and avoid needing to.

10of 32
≈ 11Min read
8Parts

It depends on the call, not the code

this is not decided where a function is written. It is decided by how it is called, and there are exactly five shapes. Learn the five and the mystery disappears.

the five
obj.method()         // this === obj        — "what is to the left of the dot"
fn()                 // undefined (strict)  — a plain call
new Fn()             // the new object
fn.call(x, a, b)     // x                   — or .apply(x, [a, b]), or .bind(x)
el.addEventListener("click", fn)   // the element

// and an arrow function has none of its own: it uses the enclosing scope

The one that breaks

Passing obj.method anywhere detaches it. The value passed is just the function; nothing remembers which object it came from.

a method that loses its object
const timer = {
  seconds: 0,
  tick() { this.seconds += 1; },
};

timer.tick();                 // fine — called with the dot
const t = timer.tick;
t();                          // TypeError: this is undefined

setTimeout(timer.tick, 1000); // same problem: the dot is gone by the time it runs

Three fixes

in order of preference
// 1. an arrow keeps the surrounding this
setTimeout(() => timer.tick(), 1000);

// 2. bind returns a new function with this fixed
setTimeout(timer.tick.bind(timer), 1000);

// 3. in a class, bind in the constructor, or use a field
class Timer {
  seconds = 0;
  tick = () => { this.seconds += 1; };   // a field holding an arrow
}
The simplest fix of all

Often the function does not need this. A plain function taking the state as an argument cannot lose it: tick(timer).

call, apply, bind

bind returns a new function; it does not change the original. Binding an already-bound function has no further effect — the first binding wins.

the same idea three ways
function describe(prefix) {
  return `${prefix} ${this.name}`;
}

describe.call({ name: "Painted" }, "The");    // arguments listed
describe.apply({ name: "Painted" }, ["The"]); // arguments in an array
const bound = describe.bind({ name: "Painted" });
bound("The");                                  // a NEW function, bound forever

// bind can pre-fill arguments too
const the = describe.bind({ name: "Veneer" }, "The");
the();                                         // "The Veneer"

Arrow functions have no this

An arrow also has no arguments, no prototype, and cannot be used with new. Those are the same trade: it is a function without its own identity.

which is right in one place and wrong in another
// RIGHT: inside a method, so the arrow inherits the method's this
const timer = {
  seconds: 0,
  start() {
    setInterval(() => { this.seconds += 1; }, 1000);
  },
};

// WRONG: as the method itself — this is the module, not the object
const broken = {
  name: "Painted",
  describe: () => `It is ${this.name}`,   // undefined
};

In an event handler

the difference is real
// a function expression: this is the element
button.addEventListener("click", function () {
  this.disabled = true;          // works
});

// an arrow: this is whatever surrounds the code
button.addEventListener("click", () => {
  this.disabled = true;          // NOT the button
  e.currentTarget.disabled = true;  // use this instead
});
Prefer currentTarget

e.currentTarget is always the element the listener is attached to, whichever function form you used. Relying on it means the handler works either way.

In a class

A field costs one function per instance; a prototype method is shared. For a handful of components it does not matter; for thousands of instances it does.

methods are on the prototype and are detachable
class Counter {
  n = 0;

  inc() { this.n += 1; }              // prototype method — loses this if detached
  dec = () => { this.n -= 1; };       // field holding an arrow — bound to the instance
}

const c = new Counter();
const inc = c.inc;
inc();                                 // TypeError

const dec = c.dec;
dec();                                 // fine

globalThis

At the top level of a module, this is undefined. In a classic script it is the global object. globalThis is the portable name for that object — window in a browser, global in Node — and you should very rarely need it.

Practice