How this works

JavaScript · Practical · 12 min

Losing this is one of the most frequent bugs in callbacks, event handlers and class methods.

this is decided by how a function is called, not where it is defined (arrows are the exception). The rules, from most to least specific:

  • `new Fn()` — this is the newly created object.
  • Explicit: fn.call(obj, a, b), fn.apply(obj, [a, b]), or a function made with fn.bind(obj) — this is obj.
  • Method call obj.method() — this is obj, the thing before the dot.
  • Plain call fn() — this is undefined in strict mode (modules and classes are always strict); in sloppy scripts it's the global object.

Arrow functions have no this of their own; they use the this of the enclosing scope at the time they are created.

The classic bug is detaching a method: const f = obj.method; f() is a plain call, so this is undefined. The same happens when you pass a method as a callback — setTimeout(obj.method, 100) or button.addEventListener("click", counter.increment). Fixes:

  • wrap it: () => obj.method()
  • bind it: obj.method.bind(obj)
  • in classes, define the handler as an arrow-function field: increment = () => { ... }

Inside a method, callbacks written as arrows see the method's this, which is why this.items.forEach(item => this.render(item)) just works. A regular function callback there would get undefined instead.

In DOM listeners registered with a regular function, this is the element the listener is attached to (event.currentTarget). Reaching for event.currentTarget is clearer anyway.

Example

"use strict";
const counter = {
  count: 0,
  increment() { this.count++; return this.count; },
  incrementLater() {
    setTimeout(() => console.log("later:", this.increment()), 0); // arrow keeps this
  },
};

console.log(counter.increment());       // 1  (method call)

const detached = counter.increment;
try { detached(); }                     // plain call: this is undefined
catch (e) { console.log(e.constructor.name); } // TypeError

const bound = counter.increment.bind(counter);
console.log(bound());                   // 2

function describe(prefix) { return `${prefix} ${this.count}`; }
console.log(describe.call(counter, "count is")); // count is 2

counter.incrementLater();               // later: 3

Remember

  • this comes from the call: new, explicit binding, obj.method(), or nothing
  • Arrow functions use the surrounding this
  • Passing obj.method as a callback loses this — wrap or bind it

Watch out for

  • Passing this.handleClick to addEventListener in a class without binding it.

Check yourself

  1. In a module: const obj = { n: 1, get() { return this.n; } }; const g = obj.get; g(); — what happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this