this is an argument, not a property
The model that makes every case fall out is this: this is an implicit parameter passed at the moment of the call. It is not stored on the function, it is not captured when the function is written, and functions do not belong to the objects you find them on. Two calls to the same function object can receive two different values, because the value comes from the call expression.
There are four ways a call supplies it, and when more than one could apply they resolve in a fixed order.
new Fn() — the highest precedence. A fresh object is created with Fn.prototype as its prototype and passed as this. If the constructor returns no object, that fresh object is the result.
fn.call(obj), fn.apply(obj), fn.bind(obj) — explicit binding. bind does not call the function; it returns a new one whose receiver is permanently fixed, which is why re-binding a bound function cannot change it.
obj.method() — implicit binding. The receiver is whatever is to the left of the final dot at the call site, evaluated fresh each time.
fn() — the default. In strict mode this is undefined; in sloppy mode it is substituted with the global object (globalThis, which is window in a browser).
new beating explicit binding is the one ordering people get wrong. Point.bind({ x: 'ignored' }) produces a constructor whose bound receiver is discarded the moment you call it with new, because the [[Construct]] path allocates its own object.
Implicit binding is a property of the call expression, so it does not survive being taken apart. The instant you write const f = obj.method, you have a plain function reference and nothing recording where it came from. Calling f() is a plain call, so the default rule applies.
class Counter {
count = 0;
increment() {
this.count += 1;
return this.count;
}
}
const c = new Counter();
console.log(c.increment());
console.log(c.increment());
const inc = c.increment;
inc();
document.body.addEventListener('click', c.increment);
Class bodies are always strict mode regardless of the surrounding file, and ES modules are too. That is why the extracted call throws rather than quietly writing count onto the global object, which is what the equivalent code in a sloppy-mode <script> would do. Sloppy mode turns this defect from a stack trace into a mutation of globalThis surfacing as wrong numbers elsewhere, which is the concrete reason strict mode is the better default and not merely the modern one.
No intermediate variable is needed for this. (0, obj.method)() also discards the receiver, because only a call whose callee is a member expression performs implicit binding.
Arrow functions capture instead
An arrow function has no this binding of its own. References to this inside it resolve like any other free variable, up the lexical scope chain to the nearest enclosing ordinary function, method, or module or script top level. That is a scoping rule rather than a binding rule, so call, apply and bind have no effect on it: they still run the function, but the thisArg is inert.
const timer = {
ticks: 0,
start() {
setInterval(() => { this.ticks += 1; }, 1000);
},
};
The distinction is not pedantry. Describing arrows as "binding this to the instance" predicts the wrong behaviour twice: it implies an arrow written as an object literal's property would see that object, when it sees whatever this was in the enclosing scope, and it implies such an arrow can be rebound. Only "no own this, resolved lexically" answers both correctly.
Choosing a fix
For a class method that will be passed around, define it as a class field holding an arrow function. It is created per instance during construction, when this is the instance, so the captured receiver is correct and no call site can undo it. The cost is one function object per instance instead of one shared on the prototype, and because it no longer lives on the prototype it cannot be overridden by a subclass in the usual way or stubbed via the prototype in tests.
bind in the constructor gives the same guarantee and keeps the prototype method intact, which is what you want when subclasses or spies need it. Wrapping at the call site, as in onClick={() => c.increment()}, leaves the method untouched and suits the case where you also pass arguments, at the price of a new closure per render. Some APIs take a receiver directly, such as the second argument to Array.prototype.map, and using it beats wrapping.
Diagnose before you fix. If this is wrong, identify which of the four call forms is being taken and what the call site actually supplies; reaching for bind without that step often just moves the problem to a different receiver.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).