If you've written JavaScript for more than a week, you've probably hit a moment where this didn't behave the way you expected. You call a method, everything works. You pass that same method as a callback, and suddenly this is undefined or points to the wrong object entirely.
The good news: this isn't random. It isn't magic. It follows one simple rule that most tutorials bury under a mountain of execution-context jargon.
Here's that rule, up front:
thisis determined by how a function is called — not where it's defined.
Everything in this post builds on that single sentence. Let's break it down.
1. What this Actually Represents
Think of this as a placeholder that answers one question every time a function runs:
"Who called me?"
this isn't a variable you set. It isn't tied to the function's location in your code. It's assigned fresh, every single time the function is invoked, based on the object standing on the left side of the dot at the call site.
caller.doSomething()
│
└── this === caller
No caller on the left of the dot? Then this falls back to a default — which changes depending on where your code is running.
2. this in the Global Context
When this is used outside of any function or object, it refers to the global context.
console.log(this);
- In a browser, this logs the
windowobject. - In Node.js at the top level of a CommonJS module, it logs
module.exports(an empty object{}), not the global object. - In strict mode (
'use strict'), top-levelthisin a plain function context becomesundefinedinstead of falling back to the global object.
Browser → this === window
Node.js → this === module.exports
Strict → this === undefined (inside functions)
The takeaway: global this is mostly a historical quirk. You rarely rely on it directly in modern JavaScript — but it explains why the "no caller" fallback exists at all.
3. this Inside Objects
This is where this starts being genuinely useful. When a function is defined as a method on an object and called through that object, this refers to the object before the dot.
const user = {
name: "Akash",
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
user.greet(); // "Hi, I'm Akash"
Here, user is the caller. user.greet() puts user on the left of the dot, so this becomes user inside greet.
user.greet()
──┬── ──┬──
│ └── function being called
└──────── this points here
Change the caller, and this changes with it — same function, different result:
const admin = {
name: "Root",
greet: user.greet
};
admin.greet(); // "Hi, I'm Root"
greet didn't change. The caller did. That's the entire rule in action.
4. this Inside Functions
Standalone functions (not attached to an object) behave differently, and this is where most confusion starts.
Regular functions
function whoAmI() {
console.log(this);
}
whoAmI(); // undefined (strict mode) or global object (non-strict)
No object called whoAmI() — it was called plain, with nothing before the dot. So this falls back to the default (global object or undefined, depending on strict mode).
The classic pitfall: losing this
const user = {
name: "Akash",
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
const fn = user.greet;
fn(); // "Hi, I'm undefined"
fn is just a reference to the function now — it's called with no caller in front of it, so this reverts to the default context. This is the single most common source of "why is this undefined" bugs, especially with callbacks and event handlers.
Arrow functions: the exception to the rule
Arrow functions don't get their own this. Instead, they capture this from the surrounding (lexical) scope where they were defined.
const user = {
name: "Akash",
greet: () => {
console.log(`Hi, I'm ${this.name}`);
}
};
user.greet(); // "Hi, I'm undefined" — this refers to outer scope, not user
This is why arrow functions are great for callbacks inside methods (they inherit this from the method), but risky as methods themselves.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // arrow function inherits `this` from start()
console.log(this.seconds);
}, 1000);
}
};
timer.start(); // works correctly, this.seconds increments
5. How Calling Context Changes this
Since this depends entirely on the call site, the same function can produce different results depending on how it's invoked. Here's the full picture in one place:
┌─────────────────────────────┬────────────────────────────┐
│ How it's called │ What `this` refers to │
├─────────────────────────────┼────────────────────────────┤
│ obj.method() │ obj │
│ method() (no object) │ undefined / global │
│ new Fn() │ the newly created instance │
│ fn.call(obj) │ obj (explicit binding) │
│ fn.apply(obj) │ obj (explicit binding) │
│ fn.bind(obj)() │ obj (permanently bound) │
│ Arrow function │ this from enclosing scope │
└─────────────────────────────┴────────────────────────────┘
A quick look at explicit binding, since it comes up constantly in real code:
function greet() {
console.log(`Hi, I'm ${this.name}`);
}
const user = { name: "Akash" };
greet.call(user); // "Hi, I'm Akash" — this = user
greet.apply(user); // "Hi, I'm Akash" — same, different syntax
const bound = greet.bind(user);
bound(); // "Hi, I'm Akash" — this permanently locked to user
call, apply, and bind exist specifically to let you choose the caller, instead of leaving it to however the function happens to get invoked.
Wrapping Up
Every confusing this bug traces back to one question: what was on the left of the dot when this function got called?
- No dot, no caller → default context (
undefinedin strict mode, global object otherwise) -
obj.method()→thisisobj - Function passed around and called plain →
thisgets lost - Arrow function →
thisisn't decided by the call at all; it's inherited from where the function was written -
call/apply/bind→ you decidethisyourself
Once you stop asking "where is this function defined?" and start asking "who called this function?", this stops being scary — it becomes one of the more predictable parts of JavaScript.
Originally published on NodeThread — backend engineering and JavaScript deep-dives.
Top comments (0)