Skip to main content

Command Palette

Search for a command to run...

The this Keyword in JavaScript: A Complete Deep Dive

Updated
12 min read
The this Keyword in JavaScript: A Complete Deep Dive
S
Software Engineer specializing in backend development and API architecture with 6+ years of experience. I build scalable web applications using PHP, REST/GraphQL APIs, and modern JavaScript, with strong expertise in WordPress, headless CMS solutions, and system integrations. Passionate about creating secure, high-performance systems that translate complex business needs into efficient technical solutions.

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

this is 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 this predictable.


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

thisdeveloper 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:

  1. Creates a brand new empty object

  2. Sets this to that new object

  3. Runs the function body

  4. 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:

  • Globalwindow or undefined (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.

More from this blog