JavaScript's this Keyword, Explained
How JavaScript determines what this refers to: default, implicit, explicit, and new binding, plus why arrow functions behave differently.
In JavaScript, this is a keyword that refers to the object a function is currently executing on — but unlike most languages, that object isn’t fixed by where the function is defined. It’s determined by how the function is called, which is exactly why this trips up so many developers coming from other languages.
There’s no single rule for what this means. Instead there are four binding rules, checked roughly in priority order, plus a fifth case for arrow functions that opts out of the whole system.
The four binding rules
Default binding. Call a plain function on its own, and this falls back to the global object (window in browsers) in non-strict mode, or undefined in strict mode (which ES modules and class bodies use by default).
function whoAmI() {
console.log(this);
}
whoAmI(); // undefined in strict mode
Implicit binding. Call a function as a method on an object, and this becomes that object.
const user = {
name: "Ava",
greet() {
console.log(this.name);
},
};
user.greet(); // "Ava"
Explicit binding. Force this to a specific value with call, apply, or bind.
function greet() {
console.log(this.name);
}
greet.call({ name: "Priya" }); // "Priya"
bind is the odd one out: instead of invoking the function, it returns a new function permanently bound to the given this, ignoring any later implicit or default binding attempts.
new binding. Call a function with new, and JavaScript creates a fresh object, sets this to it, and returns it (unless the constructor explicitly returns another object).
function Person(name) {
this.name = name;
}
const p = new Person("Sam");
When more than one rule could apply, new wins, then explicit binding, then implicit, then default. Knowing that priority order resolves most “wait, what is this here” confusion.
Arrow functions don’t have their own this
Arrow functions are the exception to all four rules above: they don’t bind this at all. Instead, they capture this lexically — from the enclosing scope at the point they’re defined, the same way closures capture variables.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // `this` is `timer`, inherited from start()
}, 1000);
},
};
Had that callback been a regular function, this inside setInterval would fall back to the global object (or undefined in strict mode), because setInterval calls the callback with no receiver — a classic instance of implicit binding being lost.
Where this gets lost
The most common this bug is passing a method as a callback and losing its receiver:
class Counter {
count = 0;
increment() {
this.count++;
}
}
const counter = new Counter();
button.addEventListener("click", counter.increment); // this is undefined inside increment
counter.increment is passed as a bare function reference. By the time the event fires, there’s no object to the left of the call — default binding kicks in, and this is undefined (in a class, which is always strict mode).
Three common fixes:
| Fix | How it works |
|---|---|
| Arrow class field | increment = () => { this.count++; } — captures this from the constructor’s scope at definition time |
.bind() in the constructor | this.increment = this.increment.bind(this) — permanently binds before the method is ever detached |
| Wrap at the call site | addEventListener("click", () => counter.increment()) — implicit binding happens inside the arrow function |
Arrow class fields are the most common modern fix because they need no constructor boilerplate, though each instance gets its own copy of the function rather than sharing one on the prototype.
this in different contexts
this behaves differently depending on where it appears:
- Top-level module code —
undefinedin ES modules (all modules run in strict mode). - Regular function, called plainly —
undefinedin strict mode, global object otherwise. - Object method — the object left of the dot at call time.
- Class method — the instance, when called as
instance.method(). - Arrow function — inherited lexically, never bound by the call.
- Event handler (
addEventListener) — the DOM element the listener is attached to, unless it’s an arrow function.
That last one is worth calling out: browser APIs like addEventListener explicitly set this to the element for a regular function handler, which is a form of implicit binding you didn’t write yourself.
Understanding this also matters once you start writing async code, since a this-losing callback bug is just as easy to introduce inside a .then() handler as inside setTimeout. If you’re moving to TypeScript, the compiler can catch some of these mistakes — strict mode flags an implicit any on this in some configurations — but it doesn’t rewrite the runtime binding rules, so the mental model above still applies.
The takeaway
this is resolved at call time, not definition time, by checking four rules in order: new, explicit (call/apply/bind), implicit (method call), and default (plain call). Arrow functions skip all of that and inherit this from their enclosing scope instead. Most real-world this bugs come from detaching a method from its object — passing obj.method as a callback — so when you hand a function off to something else to invoke, either use an arrow function, bind it explicitly, or wrap it at the call site.
Tagged
Keep reading
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.
Takina · · 4 min read JavaScript Spread vs Rest Operators, Explained
The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.
Takina · · 4 min read ResizeObserver API Explained: Watching Element Size
The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.