
JavaScript’s this depends on the call site, with explicit binding and new taking precedence; arrow functions inherit this, making them useful for callbacks but unsuitable as object methods.
Martin Ferret
November 10, 2026
One sentence solves most this bugs: the value of this is not decided where a function is written, it is decided where the function is called.
Read that again with a bug in mind. The method is correct. The object is correct. What changed is who called the function, and that is the only thing this looks at.
When a function is called as a method, this is the object to the left of the dot.
const counter = {
count: 0,
increment() {
this.count++;
}
};
counter.increment(); // `this` is counter
Nothing about increment is attached to counter. The binding happens at the moment of the call, and it lives exactly as long as that call.
Which is why this breaks:
const increment = counter.increment;
increment(); // `this` is no longer counter
Same function, no dot, no receiver. This is the single most common this bug, and it usually arrives disguised as a callback:
button.addEventListener('click', counter.increment);
The listener is called by the DOM, not by counter. Inside a listener added with addEventListener, this is the element the listener is attached to, which is also what event.currentTarget gives you.
Three methods let you decide the receiver yourself.
increment.call(counter); // arguments listed after
increment.apply(counter, []); // arguments in an array
const bound = increment.bind(counter); // returns a new function
call and apply invoke immediately and differ only in how they take arguments. bind invokes nothing: it returns a new function whose this is locked forever. Binding it again has no effect, so fn.bind(a).bind(b) is still bound to a.
bind is the fix for the callback above:
button.addEventListener('click', counter.increment.bind(counter));
new creates the receiverCalling a function with new creates a fresh object and binds this to it. That is the whole mechanism behind constructors and classes, and it outranks everything else.
With no receiver and no explicit binding, this falls back to the default. In strict mode it is undefined. In sloppy mode it is globalThis.
This is what really happened in the detached increment above. In a module, this is undefined and the call throws a TypeError on the spot. In a plain script, the same call quietly creates a count property on globalThis and increments that instead, which is the worse of the two outcomes.
It matters more than it sounds, because strict mode is not opt in anymore. Modules are always strict, and class bodies are always strict. So a detached class method fails loudly rather than corrupting the global object.
The four rules resolve in this order, highest first:
newbind, call, applyundefined in strict modeAn arrow function has no this of its own. It does not participate in the rules above at all. When you write this inside an arrow, the engine looks outward to the enclosing scope, exactly as it would for any other variable.
That is the real definition, and it explains both what arrows fix and what they break.
What they fix: a callback inside a method keeps the method's receiver.
const timer = {
seconds: 0,
start() {
setInterval(() => this.seconds++, 1000); // `this` is timer
}
};
The arrow inherits this from start, which was called on timer. No bind, no const self = this.
What they break: an arrow used as a method has nothing useful to inherit.
const counter = {
count: 0,
increment: () => {
this.count++; // not the counter
}
};
There is no enclosing method here, so this comes from the surrounding scope, which in a module is undefined. The rule of thumb writes itself: arrow functions for callbacks, regular functions for methods.
You will see this pattern in React and Angular codebases:
class Counter {
count = 0;
increment = () => {
this.count++;
};
}
As a class field, the arrow is created during construction and captures the instance. The method can be passed anywhere without losing its receiver, which is genuinely convenient.
The cost is that it is no longer a method. It lives on each instance instead of on the prototype, so every instance carries its own copy, and it cannot be overridden through the prototype chain the way a real method can.
Convenient for event handlers, wasteful for everything else.
When this surprises you, do not reread the function. Find the call site and ask what is to the left of the dot.
If the answer is nothing, you have three options: call it through its object, bind it, or wrap it in an arrow that inherits the right this. Which one depends on whether you need a reusable function or just this one call.
Ready to prove your JavaScript skills?
Get certified in JavaScriptKeep learning JavaScript. Then get certified when you are ready.

DOM Events & Modern Patterns Cheat Sheet
Learn modern DOM event handling, including propagation, delegation, listener cleanup, custom events, forms, state management, efficient mutations, and observers to build responsive, performant pages without leaks.
Martin Ferret
Nov 10, 2026

Iterator Helpers: Lazy Pipelines in JavaScript
ES2025 added map, filter, take and drop to iterators. Learn how lazy pipelines skip intermediate arrays, handle infinite sequences, and where they do not help.
Martin Ferret
Oct 6, 2026

AbortController: Cancellation and Cleanup in JavaScript
AbortController does more than cancel a fetch. Learn how one signal cleans up event listeners, times out requests, and combines with AbortSignal.any().
Martin Ferret
Sep 22, 2026