DEV Community

Cover image for Understanding the `this` Keyword in JavaScript
Akash Kumar
Akash Kumar

Posted on

Understanding the `this` Keyword in JavaScript

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:

this is 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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • In a browser, this logs the window object.
  • 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-level this in a plain function context becomes undefined instead of falling back to the global object.
Browser  →  this === window
Node.js  →  this === module.exports
Strict   →  this === undefined (inside functions)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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    │
└─────────────────────────────┴────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (undefined in strict mode, global object otherwise)
  • obj.method()this is obj
  • Function passed around and called plain → this gets lost
  • Arrow function → this isn't decided by the call at all; it's inherited from where the function was written
  • call / apply / bind → you decide this yourself

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)