No feature of JavaScript generates more "wait, why is this undefined?"
moments than this. The confusion comes from a wrong mental model: people
assume this refers to the function, or the place the function was
written. It doesn't. this is determined by how a function is called,
not where it's defined — and it's decided fresh on every single call.
Once you internalize that, the whole thing collapses into four rules with a clear priority order. Let's walk them from lowest to highest precedence.
Rule 1: default binding
A plain function call — no object, no dot — gets the global object
(window in browsers), or undefined in strict mode and ES modules
(which are always strict):
function show() {
console.log(this);
}
show(); // undefined in a module / strict mode, window otherwise
This is the source of the classic bug: pull a method off its object and
call it bare, and this is lost:
const user = {
name: "Ada",
greet() {
console.log(`Hi, ${this.name}`);
},
};
const fn = user.greet;
fn(); // "Hi, undefined" — called with no object, default binding kicks in
Rule 2: implicit binding
When a function is called as a method — object.method() — this is
the object left of the dot:
const user = {
name: "Ada",
greet() {
console.log(`Hi, ${this.name}`);
},
};
user.greet(); // "Hi, Ada" — this is user
The key word is call-time. Only the final dot matters — a.b.c.method()
binds this to c, not a. And because it's decided at the call site,
passing a method as a callback silently breaks it:
setTimeout(user.greet, 100); // "Hi, undefined" — setTimeout calls it bare
Rule 3: explicit binding
You can force this with call, apply, or bind:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const user = { name: "Ada" };
greet.call(user, "Hello"); // Hello, Ada — args listed
greet.apply(user, ["Hi"]); // Hi, Ada — args as an array
const bound = greet.bind(user); // returns a new function
bound("Hey"); // Hey, Ada — this locked to user forever
callinvokes immediately, arguments passed one by one.applyis identical but takes arguments as an array (mnemonic: apply → array).binddoesn't invoke — it returns a new function withthispermanently locked. This is the standard fix for the lost-method problem:setTimeout(user.greet.bind(user), 100).
A bound function's this cannot be overridden — not by a later call, not
by method invocation. That permanence is why bind sits above implicit
binding in priority.
Rule 4: new binding
Calling a function with new creates a fresh object and binds this to
it:
function User(name) {
this.name = name; // this = the brand-new object
}
const ada = new User("Ada");
ada.name; // "Ada"
Under the hood new creates an empty object, links its prototype, runs the
constructor with this pointing at it, and returns it. (Prototypes are
their own topic — see the
prototypes post.)
The priority order
When more than one rule could apply, they resolve in this order:
| Priority | Rule | this is… |
|---|---|---|
| 1 (highest) | new binding | the newly created object |
| 2 | Explicit (bind / call / apply) | the object you passed |
| 3 | Implicit (object.method()) | the object before the dot |
| 4 (lowest) | Default (plain call) | undefined / global object |
To find this for any call, ask the questions top-down: Was it called with
new? With call/apply, or is it a bound function? As a method off an
object? None of the above → default.
Arrow functions ignore all of it
Arrow functions don't have their own this. They capture it lexically
from the surrounding scope at definition time — and none of the four rules
can change it. This is a feature, and it fixes the most annoying this
bug in the language:
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // arrow: this is still `timer`, inherited from start()
console.log(this.seconds);
}, 1000);
},
};
timer.start(); // 1, 2, 3, … as expected
A regular function there would hit default binding (setInterval calls
its callback bare) and this.seconds would blow up. The arrow inherits
this from start, where it correctly points at timer. Before arrows,
people wrote const self = this at the top of the method to smuggle it
into the callback — the arrow makes that ceremony obsolete.
The flip side: never use an arrow as an object method if you need
this to be the object, because it'll capture the outer scope
(usually the module or global) instead:
const user = {
name: "Ada",
greet: () => console.log(this.name), // this is NOT user — captured outer
};
user.greet(); // undefined
Wrap-up
The one sentence to remember: this is set by the call, not the
definition — except for arrow functions, which freeze it at definition
and ignore the call entirely. Regular functions get their this from
whichever of the four rules applies, in priority order; arrows inherit it
from their birthplace. Trace those two paths and every this mystery in
your codebase becomes mechanical.