The this Keyword in JavaScript: A Complete Deep Dive

You've seen it everywhere. In tutorials, in codebases, in error messages. But every time you try to understand it, it slips away.
That's because this in JavaScript doesn't work like most languages. It's not fixed. It's not predictable by default. It changes based on how and where a function is called — not where it was written.
Master this, and a huge chunk of JavaScript confusion disappears.
Why Does this Even Exist?
Before we dive in — why do we need this at all?
Functions need context. When a method runs, it needs to know which object it belongs to so it can access that object's data.
const user = {
name: "Priya",
greet() {
console.log("Hello, I am " + user.name); // hardcoded — bad
}
};
What if the object's variable name changes? What if the same function is used by multiple objects? Hardcoding the object name breaks everything.
this solves this:
const user = {
name: "Priya",
greet() {
console.log("Hello, I am " + this.name); // dynamic — good
}
};
user.greet(); // Hello, I am Priya
Now the function always refers to whoever is calling it. That's the entire purpose of this.
The Golden Rule
thisis determined by how a function is called, not where it is written.
Read that twice. It's the key to understanding everything that follows.
1. Global Context — The Default Stage
When this is used outside any function or object, it refers to the global object.
console.log(this); // In browser → Window object
In a regular function called without any owner:
function showThis() {
console.log(this);
}
showThis(); // Browser → Window | Node.js → global
Strict Mode Changes Everything
In strict mode, this inside a regular function becomes undefined instead of the global object. This prevents accidental global pollution.
"use strict";
function showThis() {
console.log(this); // undefined
}
showThis();
// Without strict mode — dangerous
function setName() {
this.name = "Leaked!"; // accidentally sets window.name
}
setName();
console.log(window.name); // "Leaked!" ← global pollution
// With strict mode — safe
"use strict";
function setName() {
this.name = "Safe"; // TypeError: Cannot set property of undefined
}
Always use strict mode in modern JavaScript. It makes
thispredictable.
2. Object Methods — The Owner Rule
When a function is called as a method of an object, this refers to that object. The object before the dot is the owner.
const developer = {
name: "Rahul",
stack: "WordPress + JS",
introduce() {
console.log(`Hi, I'm \({this.name} and I work with \){this.stack}`);
}
};
developer.introduce();
// Hi, I'm Rahul and I work with WordPress + JS
this → developer because developer is before the dot.
Same Function, Different Owner
The same function can be shared between objects — this adapts:
function showRole() {
console.log(`\({this.name} is a \){this.role}`);
}
const person1 = { name: "Amit", role: "Developer", showRole };
const person2 = { name: "Sneha", role: "Designer", showRole };
person1.showRole(); // Amit is a Developer
person2.showRole(); // Sneha is a Designer
this is whoever called the function. Not where the function was defined.
3. The Lost Context Problem — The Classic Bug
This is the bug that has confused every JavaScript developer at least once.
const timer = {
message: "Time is up!",
start() {
setTimeout(function() {
console.log(this.message); // undefined ❌
}, 1000);
}
};
timer.start();
Why does this happen?
The callback passed to setTimeout is a regular function. When it runs, it's called by the browser's timer system — not by timer. So this inside it refers to window (or undefined in strict mode), not to timer.
The method start correctly has this = timer. But the inner function loses that context.
Fix 1 — Save this in a variable
The old school solution:
const timer = {
message: "Time is up!",
start() {
const self = this; // save the context
setTimeout(function() {
console.log(self.message); // ✅ Time is up!
}, 1000);
}
};
Fix 2 — Arrow Function (modern, preferred)
Arrow functions don't have their own this — they inherit it from the surrounding scope:
const timer = {
message: "Time is up!",
start() {
setTimeout(() => {
console.log(this.message); // ✅ Time is up!
}, 1000);
}
};
timer.start();
The arrow function inherits this from start(), where this is timer. Problem solved.
4. Arrow Functions — Borrowed Identity
Arrow functions do not have their own this. They look up to the scope where they were defined and borrow this from there. This is called lexical binding.
const office = {
company: "TechCorp",
employees: ["Rahul", "Priya", "Amit"],
listEmployees() {
// 'this' here = office ✅
this.employees.forEach(emp => {
// arrow function inherits 'this' from listEmployees
console.log(`\({emp} works at \){this.company}`);
});
}
};
office.listEmployees();
// Rahul works at TechCorp
// Priya works at TechCorp
// Amit works at TechCorp
Now the same with a regular function inside forEach — it breaks:
const office = {
company: "TechCorp",
employees: ["Rahul", "Priya", "Amit"],
listEmployees() {
this.employees.forEach(function(emp) {
console.log(`\({emp} works at \){this.company}`); // undefined ❌
});
}
};
When NOT to use arrow functions
Arrow functions are great for callbacks — but don't use them as object methods:
const user = {
name: "Priya",
greet: () => {
console.log("Hello, " + this.name); // undefined ❌
}
};
user.greet();
Because arrow functions don't have their own this, they look outside the object — and find the global context, not the object. Always use regular functions for object methods.
5. call(), apply(), bind() — Taking Control
Sometimes you need to manually set what this should be. JavaScript gives you three methods for this.
call() — Run now, pass arguments one by one
function introduce(city, country) {
console.log(`I'm \({this.name} from \){city}, ${country}`);
}
const person = { name: "Karan" };
introduce.call(person, "Mumbai", "India");
// I'm Karan from Mumbai, India
apply() — Run now, pass arguments as an array
Same as call() but arguments go in an array:
introduce.apply(person, ["Mumbai", "India"]);
// I'm Karan from Mumbai, India
When to use apply? When your arguments are already in an array:
const args = ["Delhi", "India"];
introduce.apply(person, args); // clean!
bind() — Lock context, run later
bind() doesn't call the function immediately. It returns a new function with this permanently locked in:
function greet() {
console.log(`Hello from ${this.city}`);
}
const mumbaiContext = { city: "Mumbai" };
const greetMumbai = greet.bind(mumbaiContext);
// call it anytime, anywhere — this is always mumbaiContext
greetMumbai(); // Hello from Mumbai
greetMumbai(); // Hello from Mumbai
Real use case — event handlers:
const button = {
label: "Submit",
handleClick() {
console.log(`${this.label} was clicked`);
}
};
// Without bind — loses context
document.querySelector("btn").addEventListener("click", button.handleClick);
// ❌ 'this' becomes the DOM element, not button
// With bind — context locked
document.querySelector("btn").addEventListener("click", button.handleClick.bind(button));
// ✅ Submit was clicked
Quick Comparison
| Method | Calls immediately? | Arguments | Use case |
|---|---|---|---|
call() |
✅ Yes | One by one | Borrow a method once |
apply() |
✅ Yes | As array | Same as call, args in array |
bind() |
❌ No (returns fn) | One by one | Lock context for later use |
6. new Keyword — Building Fresh Instances
When you call a function with new, JavaScript does four things automatically:
Creates a brand new empty object
Sets
thisto that new objectRuns the function body
Returns the new object
function Person(name, role) {
this.name = name;
this.role = role;
this.introduce = function() {
console.log(`I'm \({this.name}, a \){this.role}`);
};
}
const dev1 = new Person("Rahul", "Developer");
const dev2 = new Person("Priya", "Designer");
dev1.introduce(); // I'm Rahul, a Developer
dev2.introduce(); // I'm Priya, a Designer
Each call to new Person() creates a separate object with its own name and role.
The Prototype Optimization
There's a problem with the above — every instance gets its own copy of introduce. If you create 1000 users, you have 1000 copies of the same function in memory.
The solution: put methods on the prototype:
function Person(name, role) {
this.name = name;
this.role = role;
}
// Shared across ALL instances — one copy in memory
Person.prototype.introduce = function() {
console.log(`I'm \({this.name}, a \){this.role}`);
};
const dev1 = new Person("Rahul", "Developer");
const dev2 = new Person("Priya", "Designer");
dev1.introduce(); // I'm Rahul, a Developer
dev2.introduce(); // I'm Priya, a Designer
// Both share the same introduce function
console.log(dev1.introduce === dev2.introduce); // true ✅
this inside introduce still correctly refers to whichever instance called it — even though the function lives on the prototype.
7. this in Event Listeners
In DOM event listeners, this refers to the element that fired the event:
const btn = document.querySelector("#myBtn");
btn.addEventListener("click", function() {
console.log(this); // <button id="myBtn"> — the element itself
this.style.backgroundColor = "green"; // works ✅
});
But with an arrow function:
btn.addEventListener("click", () => {
console.log(this); // Window ❌ — arrow inherits global this
this.style.backgroundColor = "green"; // TypeError
});
Use regular functions in event listeners when you need this to refer to the element.
8. this in Classes
ES6 classes make this cleaner and more predictable:
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
console.log(`\({this.owner} deposited ₹\){amount}. Balance: ₹${this.balance}`);
}
withdraw(amount) {
if (amount > this.balance) {
console.log("Insufficient funds");
return;
}
this.balance -= amount;
console.log(`\({this.owner} withdrew ₹\){amount}. Balance: ₹${this.balance}`);
}
}
const account = new BankAccount("Priya", 5000);
account.deposit(2000); // Priya deposited ₹2000. Balance: ₹7000
account.withdraw(1000); // Priya withdrew ₹1000. Balance: ₹6000
In classes, this always refers to the instance created by new. Clean and predictable.
Class + Arrow Method for Callbacks
When passing class methods as callbacks, this can still get lost:
class Counter {
constructor() {
this.count = 0;
}
// regular method — loses 'this' when passed as callback
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
setTimeout(counter.increment, 1000); // NaN ❌ — 'this' lost
// Fix: bind in constructor
class Counter {
constructor() {
this.count = 0;
this.increment = this.increment.bind(this); // lock it
}
increment() {
this.count++;
console.log(this.count); // ✅ 1
}
}
9. globalThis — The Modern Cross-Environment Solution
window exists in browsers. global exists in Node.js. Remembering which one to use is annoying. ES2020 introduced globalThis — it works everywhere:
console.log(globalThis); // Window in browser, global in Node.js
globalThis.appName = "MyApp";
console.log(globalThis.appName); // MyApp — works in any environment
Use globalThis when you need to access the global object in code that runs in multiple environments.
The Complete Mental Model
How is the function called?
│
├── With new? → this = new empty object
│
├── With call/apply? → this = first argument
│
├── With bind? → this = bound object (permanent)
│
├── As object method? → this = object before the dot
│
├── Arrow function? → this = inherited from parent scope
│
├── Event listener? → this = DOM element (regular fn)
│
└── Plain call? → this = global (or undefined in strict)
Common Bugs and Fixes at a Glance
// ❌ BUG 1 — lost context in callback
const obj = {
val: 42,
run() { setTimeout(function() { console.log(this.val); }, 0); }
};
// ✅ FIX — use arrow function
const obj = {
val: 42,
run() { setTimeout(() => { console.log(this.val); }, 0); } // 42
};
// ❌ BUG 2 — arrow function as object method
const obj = { name: "Test", greet: () => console.log(this.name) };
// ✅ FIX — use regular function
const obj = { name: "Test", greet() { console.log(this.name); } }; // Test
// ❌ BUG 3 — detached method loses context
const greet = obj.greet;
greet(); // undefined
// ✅ FIX — bind it
const greet = obj.greet.bind(obj);
greet(); // Test
this vs Closures — The Core Distinction
Your LinkedIn post nailed this, so let's make it code-clear:
function makeCounter() {
let count = 0; // closure data — the "briefcase"
return {
increment() {
count++; // closure: accesses 'count' from outer scope
console.log(this); // this: refers to the returned object
console.log(count); // closure data — always accessible
}
};
}
const counter = makeCounter();
counter.increment();
// this → { increment: f } ← the returned object (WHO)
// count → 1 ← closure variable (WHAT)
this |
Closure | |
|---|---|---|
| Answers | Who is calling? | What data to access? |
| Determined by | How function is called | Where function is defined |
| Can be lost? | ✅ Yes | ❌ No |
| Fixed with | bind / arrow fn | Nothing needed |
Wrapping Up
this is not magic. It follows clear, predictable rules once you understand the context it runs in. Here's the one-line summary for each case:
Global →
windoworundefined(strict)Object method → the object before the dot
Arrow function → inherited from parent scope
call / apply → whatever you pass as first argument
bind → permanently locked to the bound object
new → the freshly created instance
Event listener → the DOM element that fired the event
Class → the instance created by
new
Once you stop thinking of this as a variable and start thinking of it as runtime context, everything clicks.





