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()` —
thisis the newly created object. - Explicit:
fn.call(obj, a, b),fn.apply(obj, [a, b]), or a function made withfn.bind(obj)—thisisobj. - Method call
obj.method()—thisisobj, the thing before the dot. - Plain call
fn()—thisisundefinedin 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: 3Remember
thiscomes from the call:new, explicit binding,obj.method(), or nothing- Arrow functions use the surrounding
this - Passing
obj.methodas a callback losesthis— wrap or bind it
Watch out for
- Passing
this.handleClicktoaddEventListenerin a class without binding it.