Skip to main content

Command Palette

Search for a command to run...

Understanding the `this` Keyword in JavaScript: What It Is and How Calling Context Controls It

Updated
7 min read
Understanding the `this` Keyword in JavaScript: What It Is and How Calling Context Controls It

Understanding the this Keyword in JavaScript: What It Is and How Calling Context Controls It

TL;DR: this in JavaScript refers to the object that invoked the function — not where the function was defined. Its value is determined at call time, not at write time. Once you internalize that mental model, nearly every this confusion disappears.

Audience: This post targets developers who know basic JavaScript but get tripped up by this behaving unexpectedly in callbacks, event handlers, or object methods.


Problem

You write a method on an object, it works fine when called directly — then you pass it as a callback and it breaks. this is suddenly undefined or pointing at window. You've seen this before:

const user = {
  name: "Riya",
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

const greetFn = user.greet;
greetFn(); // Hello, I'm undefined

The function didn't change. The caller did. That's the entire story of this.


Solution: The Core Mental Model

this = the object to the left of the dot when the function is called.

If there's no object to the left of the dot, this falls back to the global object (window in browsers, global in Node.js) — or undefined in strict mode.

This is called implicit binding and it's the default rule.

Caller → Functionthis

When user.greet() is called, user is to the left of the dot. So this = user. When greetFn() is called with no object, there's nothing to the left. So this = undefined (strict) or window (non-strict).


Step 1: this in the Global Context

In a browser, code running at the top level has this pointing to window.

// Run this in a browser console (non-strict mode)
console.log(this === window); // true

this.appName = "MyApp";
console.log(window.appName); // "MyApp"

In Node.js, the top-level this inside a module is an empty object {}, not global. This surprises people:

// In a Node.js module file
console.log(this); // {}
console.log(this === global); // false

// But inside a regular function at top level (non-strict):
function showContext() {
  console.log(this === global);
}
showContext(); // true

Why: Node.js wraps each file in a module wrapper function. The top-level this is the module's exports object, not global.


Step 2: this Inside Object Methods

This is where this is most intuitive. The object calling the method becomes this.

const account = {
  owner: "Arjun",
  balance: 5000,
  showBalance() {
    console.log(`${this.owner} has ₹${this.balance}`);
  }
};

account.showBalance();
// Output: Arjun has ₹5000

account is to the left of .showBalance(), so this = account.

Now a nested object example — this is where developers get confused:

const bank = {
  name: "NeoBank",
  account: {
    owner: "Priya",
    showOwner() {
      console.log(this.owner);      // "Priya" ✅
      console.log(this.name);       // undefined ❌ (this is account, not bank)
    }
  }
};

bank.account.showOwner();
// Output:
// Priya
// undefined

Why: The immediate caller is bank.account, not bank. this only goes one level up — to the direct caller.


Step 3: this Inside Regular Functions

A standalone function call has no object context. In non-strict mode, this defaults to the global object. In strict mode, it's undefined.

"use strict";

function identify() {
  console.log(this);
}

identify(); // undefined (strict mode)
// Without strict mode
function identify() {
  console.log(this === window); // true (in browser)
}

identify();

This becomes a real problem inside object methods when you use a helper function:

"use strict";

const cart = {
  items: ["apple", "mango"],
  printItems() {
    this.items.forEach(function(item) {
      console.log(this); // undefined — regular function, lost context
    });
  }
};

cart.printItems();

The forEach callback is a regular function. It's not called as cart.something(). There's no object to the left of the dot — so this is undefined in strict mode.


Step 4: How Calling Context Changes this

The same function can have different values of this depending on how it's called.

const order = {
  id: "ORD-001",
  printId() {
    console.log(`Order: ${this.id}`);
  }
};

const delivery = {
  id: "DEL-999"
};

// Normal call — this = order
order.printId(); // Order: ORD-001

// Borrow the method — this = delivery
delivery.printId = order.printId;
delivery.printId(); // Order: DEL-999

// Detach — this = undefined (strict) or window
const standalone = order.printId;
standalone(); // Order: undefined

Same function. Three different results. Caller determines this.


Step 5: Fixing Lost this — Arrow Functions

Arrow functions do not have their own this. They inherit this from the surrounding lexical scope at the time they are defined.

"use strict";

const cart = {
  items: ["apple", "mango"],
  printItems() {
    // Arrow function captures `this` from printItems — which is cart
    this.items.forEach((item) => {
      console.log(`${this.items.indexOf(item) + 1}. ${item}`);
    });
  }
};

cart.printItems();
// Output:
// 1. apple
// 2. mango

This works because the arrow function doesn't create its own this. It uses the this from printItems, which is cart.

Important: Never use arrow functions as object methods if you need this to refer to the object.

const profile = {
  name: "Dev",
  // ❌ Arrow function as method — this is NOT profile
  greet: () => {
    console.log(`Hello, ${this.name}`);
  }
};

profile.greet(); // Hello, undefined

Why? The arrow function was defined in the outer scope (likely global or module scope), so this is that outer scope — not profile.


Step 6: Explicit Binding with call, apply, and bind

You can manually set this using these three methods.

function introduce(role, city) {
  console.log(`I'm ${this.name}, a ${role} from ${city}`);
}

const dev = { name: "Sneha" };

// call — invoke immediately, args passed one by one
introduce.call(dev, "frontend engineer", "Pune");
// I'm Sneha, a frontend engineer from Pune

// apply — invoke immediately, args passed as array
introduce.apply(dev, ["backend engineer", "Bengaluru"]);
// I'm Sneha, a backend engineer from Bengaluru

// bind — returns a new function with this permanently set
const boundIntroduce = introduce.bind(dev, "fullstack engineer");
boundIntroduce("Mumbai");
// I'm Sneha, a fullstack engineer from Mumbai

bind is especially useful when passing methods as callbacks:

const timer = {
  label: "Request Timer",
  start() {
    console.log(`${this.label} started`);
  }
};

// Without bind — this is lost
setTimeout(timer.start, 1000);             // undefined started

// With bind — this is preserved
setTimeout(timer.start.bind(timer), 1000); // Request Timer started

Results

After applying these rules, here's a quick reference:

Call Patternthis Value
obj.method()obj
method() (no object)undefined (strict) / window (non-strict)
Arrow function inside methodInherited from surrounding scope
Arrow function as methodOuter scope (window / undefined)
fn.call(obj)obj
fn.apply(obj)obj
fn.bind(obj)()obj

Trade-offs

Arrow functions aren't always the fix. They solve the callback problem but break method definitions on objects. Choosing between arrow and regular functions requires knowing whether you want this to be dynamic (regular) or lexically inherited (arrow).

bind creates a new function every time it's called. Calling fn.bind(obj) inside render() or a loop creates a new function reference on each call — that has memory implications in hot paths.

this is invisible in the function signature. Unlike parameters, the this binding isn't visible when reading a function definition. You have to trace the call site, which makes codebases harder to reason about at scale. This is one reason many teams prefer passing context explicitly as a parameter over relying on this.


Conclusion

this is determined by who calls the function, not where it's defined. That single sentence resolves most confusion. When you see unexpected this behavior:

  1. Find the call site
  2. Check what's to the left of the dot
  3. If nothing is there, apply the fallback rules (global or undefined)
  4. If using a callback, check whether it's an arrow function or a regular one

Next step: Practice by predicting this before running code. Write a function, call it three different ways (direct, detached, via call), and verify your predictions against the output.


Further Reading