Vikram D.

What Exactly is 'this' in JavaScript?

2026-01-0312 min read
What Exactly is 'this' in JavaScript?

If you've ever scratched your head wondering why this is undefined, or why it suddenly points to the window object when you expected it to be your class instance, you're not alone. The this keyword is one of the most misunderstood concepts in JavaScript.

In this post, we'll demystify this once and for all.

The Golden Rule

Here's the key insight that will save you hours of debugging:

this is determined by HOW a function is called, not WHERE it's defined.

This is fundamentally different from languages like Java or C++ where this always refers to the current object instance. In JavaScript, this is dynamic and depends on the call site.

The Four Rules of this

JavaScript determines the value of this using four rules, applied in order of precedence:

1. Default Binding (Standalone Function Call)

When a function is called without any context, this defaults to:

  • undefined in strict mode
  • The global object (window in browsers, global in Node.js) in non-strict mode
function showThis() {
  console.log(this);
}

showThis(); // undefined (strict mode) or window (non-strict)

This is often the source of bugs:

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

const greetFn = user.greet;
greetFn(); // "Hello, undefined" — `this` is not the user object!

When we assigned user.greet to a variable and called it standalone, we lost the implicit binding.

2. Implicit Binding (Method Call)

When a function is called as a method of an object, this refers to that object.

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

user.greet(); // "Hello, Vikram" ✅

The rule is simple: look at what's left of the dot at the call site.

const user = {
  name: "Vikram",
  address: {
    city: "Hyderabad",
    getCity() {
      console.log(this.city);
    }
  }
};

user.address.getCity(); // "Hyderabad" — `this` is `user.address`, not `user`

3. Explicit Binding (call, apply, bind)

You can explicitly set this using these methods:

function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const user = { name: "Vikram" };

// call — pass arguments individually
greet.call(user, "Hello"); // "Hello, Vikram"

// apply — pass arguments as an array
greet.apply(user, ["Hi"]); // "Hi, Vikram"

// bind — returns a new function with `this` permanently bound
const boundGreet = greet.bind(user);
boundGreet("Hey"); // "Hey, Vikram"

bind is particularly useful for event handlers and callbacks:

class Button {
  constructor(label) {
    this.label = label;
  }

  click() {
    console.log(`${this.label} clicked`);
  }
}

const btn = new Button("Submit");
document.querySelector("button").addEventListener("click", btn.click.bind(btn));

4. new Binding (Constructor Call)

When a function is called with new, JavaScript:

  1. Creates a new empty object
  2. Sets this to that new object
  3. Executes the constructor
  4. Returns the object (unless the constructor returns something else)
function User(name) {
  this.name = name;
}

const vikram = new User("Vikram");
console.log(vikram.name); // "Vikram"

With ES6 classes:

class User {
  constructor(name) {
    this.name = name; // `this` is the new instance
  }
}

Arrow Functions: A Different Beast

Arrow functions don't have their own this. Instead, they lexically inherit this from the enclosing scope at the time they're defined.

const user = {
  name: "Vikram",
  greet: () => {
    console.log(`Hello, ${this.name}`);
  }
};

user.greet(); // "Hello, undefined" — arrow function uses outer `this`!

This might seem like a limitation, but it's actually incredibly useful for callbacks:

const user = {
  name: "Vikram",
  friends: ["Alice", "Bob"],
  
  // ❌ Regular function loses `this`
  showFriendsBroken() {
    this.friends.forEach(function(friend) {
      console.log(`${this.name} knows ${friend}`); // `this` is undefined!
    });
  },
  
  // ✅ Arrow function preserves `this`
  showFriends() {
    this.friends.forEach((friend) => {
      console.log(`${this.name} knows ${friend}`); // Works!
    });
  }
};

user.showFriends();
// "Vikram knows Alice"
// "Vikram knows Bob"

Arrow Functions Cannot Be Rebound

You cannot change an arrow function's this with call, apply, or bind:

const greet = () => {
  console.log(this);
};

const user = { name: "Vikram" };

greet.call(user); // Still the outer `this`, not `user`
greet.apply(user); // Same
greet.bind(user)(); // Same

Method Borrowing

Method borrowing is a technique where you use a method from one object on another object using call or apply. This is possible because this is set at call time.

Borrowing Array Methods

Array-like objects (like arguments, NodeLists, or strings) don't have array methods. You can borrow them:

function sum() {
  // `arguments` is array-like but not an array
  const args = Array.prototype.slice.call(arguments);
  return args.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3, 4); // 10

Modern alternative using spread:

function sum(...args) {
  return args.reduce((a, b) => a + b, 0);
}

Borrowing Methods Between Objects

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

const dog = {
  name: "Buddy"
};

// Borrow the greet method for dog
person.greet.call(dog); // "Hello, I'm Buddy"

Practical Example: Using Math Methods on Arrays

const numbers = [5, 2, 9, 1, 7];

// Math.max expects individual arguments, not an array
Math.max(5, 2, 9, 1, 7); // 9

// Borrow using apply (passes array as individual args)
Math.max.apply(null, numbers); // 9

// Modern alternative: spread operator
Math.max(...numbers); // 9

Common Gotchas and How to Fix Them

Gotcha 1: Losing this in Callbacks

class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // ❌ `this` will be undefined inside the callback
    setInterval(function() {
      this.seconds++; // TypeError: Cannot read property 'seconds' of undefined
      console.log(this.seconds);
    }, 1000);
  }
}

Solutions:

// Solution 1: Arrow function
start() {
  setInterval(() => {
    this.seconds++;
    console.log(this.seconds);
  }, 1000);
}

// Solution 2: Bind the callback
start() {
  setInterval(function() {
    this.seconds++;
    console.log(this.seconds);
  }.bind(this), 1000);
}

// Solution 3: Store `this` in a variable (old-school)
start() {
  const self = this;
  setInterval(function() {
    self.seconds++;
    console.log(self.seconds);
  }, 1000);
}

Gotcha 2: Event Handlers in Classes

class Toggle {
  constructor(element) {
    this.isOn = false;
    this.element = element;
    
    // ❌ `this` will be the DOM element, not the Toggle instance
    element.addEventListener("click", this.toggle);
  }

  toggle() {
    this.isOn = !this.isOn; // `this` is the <button>, not Toggle!
    console.log(this.isOn);
  }
}

Solution: Use arrow functions or bind

class Toggle {
  constructor(element) {
    this.isOn = false;
    element.addEventListener("click", () => this.toggle());
    // OR: element.addEventListener("click", this.toggle.bind(this));
  }

  toggle() {
    this.isOn = !this.isOn;
    console.log(this.isOn);
  }
}

Or use class field arrow functions:

class Toggle {
  isOn = false;
  
  // Arrow function as class field — `this` is always the instance
  toggle = () => {
    this.isOn = !this.isOn;
    console.log(this.isOn);
  }

  constructor(element) {
    element.addEventListener("click", this.toggle);
  }
}

Gotcha 3: Destructuring Methods

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

const { greet } = user;
greet(); // "Hello, undefined" — destructuring loses the binding!

Solution: Don't destructure methods you intend to call, or bind them:

const greet = user.greet.bind(user);
greet(); // "Hello, Vikram"

Gotcha 4: this in Nested Functions

const user = {
  name: "Vikram",
  outer() {
    console.log(this.name); // "Vikram" ✅
    
    function inner() {
      console.log(this.name); // undefined ❌
    }
    inner();
  }
};

user.outer();

The inner function is a standalone call, so it gets default binding.

Solution: Arrow function

outer() {
  console.log(this.name);
  
  const inner = () => {
    console.log(this.name); // "Vikram" ✅
  };
  inner();
}

Interview Questions

Interview Question: What is the this keyword in JavaScript?

Answer: this is a special keyword that refers to the object that is executing the current function. Unlike other languages where this always refers to the instance, in JavaScript this is determined by how a function is called, not where it's defined. Its value depends on the call site and can be the global object, an instance, an explicitly bound object, or undefined.

Interview Question: What is the difference between call, apply, and bind?

Answer: All three set this explicitly. call invokes the function immediately with arguments passed individually: fn.call(obj, arg1, arg2). apply invokes immediately with arguments as an array: fn.apply(obj, [arg1, arg2]). bind does NOT invoke the function - it returns a new function with this permanently bound: const boundFn = fn.bind(obj).

Interview Question: How is this different in arrow functions vs regular functions?

Answer: Arrow functions don't have their own this. They inherit this lexically from the enclosing scope at the time they're defined. Regular functions determine this at call time based on how they're called. You cannot rebind an arrow function's this using call, apply, or bind.

Interview Question: What happens to this when you assign a method to a variable and call it?

Answer: You lose implicit binding. When you assign obj.method to a variable and call it standalone, this becomes undefined (strict mode) or the global object. To preserve this, use bind: const fn = obj.method.bind(obj).

Interview Question: How do you fix this issues in class event handlers?

Answer: Three solutions:

  1. Use arrow function wrapper: element.addEventListener('click', () => this.handleClick())
  2. Use bind: element.addEventListener('click', this.handleClick.bind(this))
  3. Define the method as a class field arrow function: handleClick = () => { ... } which automatically binds this to the instance

Interview Question: What is method borrowing in JavaScript?

Answer: Method borrowing is using a method from one object on another object using call or apply. For example, array-like objects (like arguments or NodeLists) don't have array methods, but you can borrow them: Array.prototype.slice.call(arguments) or [].forEach.call(nodeList, fn). This works because you're explicitly setting this to the target object.

Quick Reference Table

Call Stylethis Value
func()undefined (strict) or global
obj.func()obj
func.call(obj)obj
func.apply(obj)obj
func.bind(obj)()obj
new Func()The new instance
Arrow functionLexical (inherited from enclosing scope)

Summary

  1. Default binding: Standalone calls get undefined (strict) or global
  2. Implicit binding: Method calls bind this to the object before the dot
  3. Explicit binding: call, apply, bind let you set this manually
  4. new binding: Constructor calls set this to the new instance
  5. Arrow functions: Don't have their own this-they inherit from the enclosing scope

The key takeaway? Always think about how a function is called, not where it's written. When in doubt, use arrow functions for callbacks and bind for event handlers.

Loading comments...