Skip to main content

Command Palette

Search for a command to run...

JavaScript this Keyword Explained Simply

Updated
12 min readView as Markdown
JavaScript this Keyword Explained Simply
M
Software Engineer exploring software, networking, and systems. I document what I learn, share practical insights, and learn from the community.

If you've written JavaScript for a while, you've probably seen this:

this.name

It looks simple.

Then you put it inside a function and suddenly this becomes undefined, a global object, or something completely different.

Why?

Because one of the most common explanations of this is also one of the most misleading:

"this refers to the current object."

That's only sometimes true.

A much better mental model is:

For regular functions, this is determined by how the function is called.

And there's one major exception:

Arrow functions don't have their own this; they inherit it from their surrounding scope.

Once you understand those two ideas, most this behavior becomes predictable.


The Core Mental Model

For a regular function, first look at the call:

How was the function called?
          │
    ┌─────┼──────────┐
    ↓     ↓          ↓
object.  fn()       new Fn()
method()
    │     │          │
    ↓     ↓          ↓
 object  depends    new object
         on mode

But for an arrow function:

Arrow function
      │
      ↓
Doesn't create its own `this`
      │
      ↓
Uses surrounding `this`

This distinction explains most of JavaScript's this.


this in an Object Method

The easiest case:

const user = {
  name: "Maaz",

  greet() {
    console.log(this.name);
  }
};

user.greet();

The function is called as:

user.greet();

Therefore:

user.greet()
     ↓
this = user

So:

this.name

is effectively:

user.name

Output:

Maaz

This is why the common pattern:

object.method();

usually gives:

this → object

this Is Not Where the Function Was Defined

Here's the important part.

const user = {
  name: "Maaz",

  greet() {
    console.log(this.name);
  }
};

user.greet();

works because of the call site.

But:

const greet = user.greet;

greet();

is a different call.

The function is now called as:

greet();

not:

user.greet();

The original receiver is gone.

So don't think:

"this belongs to the object where the function was created."

Instead ask:

"How is this function being called right now?"


Regular Functions

The same this rules apply whether you create a regular function using a declaration:

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

or a function expression:

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

Both are regular functions.

Their this is determined by how they're invoked.

For example:

const user = {
  name: "Maaz",

  greet: function () {
    console.log(this.name);
  }
};

user.greet();

Here:

this → user

The important distinction isn't:

function declaration vs function expression

It's:

regular function vs arrow function

this in a Normal Function Call

Consider:

"use strict";

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

showThis();

There is no object before the function call.

It's simply:

showThis();

In strict mode:

this → undefined

In non-strict classic script code, a normal function call can default this to the global object.

So:

Regular function
      │
      ├── strict mode
      │      ↓
      │   undefined
      │
      └── non-strict
             ↓
        global object

This is why:

"this always means the caller"

is not a completely accurate rule.

It's better to say:

Regular functions get this from their invocation pattern.


Global this

Top-level this is a separate case from function this.

In a traditional browser script:

console.log(this === window);

generally gives:

true

But ES modules behave differently:

// module.js

console.log(this);

At the top level of an ES module:

this → undefined

Node.js can also differ depending on whether you're using CommonJS or ES modules.

So don't memorize:

this = window

as a universal JavaScript rule.

Instead ask:

Where is this code running?
Script?
Module?
Function?
Method?

Arrow Functions: The Big Exception

Now consider:

const user = {
  name: "Maaz",

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

user.greet();

You might expect:

this → user

But arrow functions don't work that way.

An arrow function doesn't create its own this.

Instead, it inherits this from its surrounding lexical scope.

Think:

Arrow function
      │
      ↓
No own `this`
      │
      ↓
Look at surrounding scope
      │
      ↓
Use that `this`

Therefore, an arrow function used as an object method does not automatically get the object as this.


Regular Function vs Arrow Function

Compare:

const user = {
  name: "Maaz",

  regular() {
    console.log(this.name);
  },

  arrow: () => {
    console.log(this.name);
  }
};

user.regular();
user.arrow();

The regular method:

user.regular()
      ↓
this = user

The arrow:

user.arrow()
      ↓
arrow has no own this
      ↓
uses surrounding this

This is why, when an object method needs dynamic this, the normal method syntax is usually the better choice:

const user = {
  name: "Maaz",

  greet() {
    console.log(this.name);
  }
};

rather than:

const user = {
  name: "Maaz",

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

Nested Functions: Where Things Get Interesting

Consider:

const user = {
  name: "Maaz",

  greet() {
    console.log(this.name);

    function inner() {
      console.log(this.name);
    }

    inner();
  }
};

user.greet();

The outer method is called as:

user.greet();

so:

greet()
  ↓
this = user

But inner() is called separately:

inner();

It's another regular function call.

It does not automatically inherit the outer this.

Now change it to an arrow:

const user = {
  name: "Maaz",

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

    inner();
  }
};

user.greet();

Now the arrow inherits this from greet():

user.greet()
      ↓
this = user
      ↓
inner arrow
      ↓
inherits this
      ↓
user

This is one of the most practical reasons arrow functions are useful.


this in Callbacks

The same concept appears constantly in asynchronous JavaScript.

This can cause problems:

const user = {
  name: "Maaz",

  greet() {
    setTimeout(function () {
      console.log(this.name);
    }, 1000);
  }
};

The callback is a separate regular function.

It doesn't automatically inherit the outer method's this.

An arrow callback solves that:

const user = {
  name: "Maaz",

  greet() {
    setTimeout(() => {
      console.log(this.name);
    }, 1000);
  }
};

Now:

greet()
  ↓
this = user
  ↓
arrow callback
  ↓
inherits this
  ↓
user

This pattern appears frequently with:

setTimeout
Promises
array callbacks
event handlers
async code

this in Event Handlers

Browser event handlers are another important case.

With a regular function:

button.addEventListener("click", function () {
  console.log(this);
});

the browser sets this to the element handling the event.

So:

regular event listener
        ↓
this = currentTarget

With an arrow:

button.addEventListener("click", () => {
  console.log(this);
});

the arrow doesn't get the button as this.

It inherits this from the surrounding scope.

That's why arrow-based event handlers commonly use:

button.addEventListener("click", (event) => {
  console.log(event.currentTarget);
});

instead.

Also remember:

this / event.currentTarget
        ↓
element whose listener is handling the event

event.target
        ↓
element where the event actually originated

These aren't always the same element.


call(), apply(), and bind()

JavaScript gives you explicit control over this.

call()

function greet() {
  console.log(this.name);
}

const user = {
  name: "Maaz"
};

greet.call(user);

Now:

this → user

call() executes the function immediately.


apply()

apply() works similarly but accepts arguments as an array:

function introduce(age, city) {
  console.log(this.name, age, city);
}

introduce.apply(user, [23, "Multan"]);

Think:

call(obj, arg1, arg2)
apply(obj, [arg1, arg2])

bind()

bind() doesn't immediately execute the function.

It creates a new function with this bound:

const boundGreet = greet.bind(user);

boundGreet();

Now:

boundGreet()
     ↓
this = user

This is especially useful when passing object methods as callbacks.


Can call() Change Arrow Function this?

No.

For example:

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

greet.call(user);

call() cannot replace an arrow function's lexical this.

The same applies to:

apply()
bind()

So:

Regular function
call/apply/bind → can control this


Arrow function
call/apply/bind → cannot replace lexical this

this With new

Now consider a constructor function:

function Student(name, age) {
  this.name = name;
  this.age = age;
}

const student = new Student("Maaz", 23);

With new, JavaScript creates a new object and uses that object as this.

A useful simplified model is:

new Student(...)
      │
      ↓
create new object
      │
      ↓
link to Student.prototype
      │
      ↓
run constructor with this = object
      │
      ↓
return object

Therefore:

student.name

gives:

Maaz

The same principle is used by classes:

class Student {
  constructor(name) {
    this.name = name;
  }
}

const student = new Student("Maaz");

Inside the constructor:

this → newly created Student instance

this in Class Methods

Consider:

class Student {
  constructor(name) {
    this.name = name;
  }

  introduce() {
    console.log(this.name);
  }
}

const student = new Student("Maaz");

student.introduce();

Because the method is called as:

student.introduce();

we get:

this → student

But if we extract it:

const introduce = student.introduce;

introduce();

the method loses its receiver.

Since class methods are strict-mode functions:

this → undefined

This is a common source of bugs when passing class methods as callbacks.


Static Methods

Static methods belong to the class rather than an instance:

class MathHelper {
  static square(number) {
    return number * number;
  }
}

MathHelper.square(5);

Inside the static method:

this → MathHelper

So:

Instance method
student.introduce()
        ↓
this = student


Static method
MathHelper.square()
        ↓
this = MathHelper

Getters and Setters

this also works inside getters and setters:

const user = {
  firstName: "Maaz",
  lastName: "Hafeez",

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
};

console.log(user.fullName);

Here this refers to the object on which the getter is accessed.

Setters work similarly:

const user = {
  _name: "",

  set name(value) {
    this._name = value;
  }
};

user.name = "Maaz";

So this isn't limited to ordinary methods.


A Powerful Example: Method Borrowing

Regular functions can be reused with different objects.

function introduce() {
  console.log(`My name is ${this.name}`);
}

const user1 = {
  name: "Maaz"
};

const user2 = {
  name: "Ali"
};

introduce.call(user1);
introduce.call(user2);

Output:

My name is Maaz
My name is Ali

Same function.

Different this.

That's the power of JavaScript's dynamic function context.


The this Debugging Checklist

When this behaves unexpectedly, don't guess.

Ask these questions:

1. Is it an arrow function?

If yes:

It doesn't have its own this.
Look at the surrounding scope.

2. Is it called with new?

new Constructor()

Then:

this → new object

3. Is call, apply, or bind involved?

If it's a regular function, they can explicitly control this.

4. Is it called as object.method()?

Then:

this → object

5. Was the method extracted?

Look for:

const fn = object.method;

or:

const { method } = object;

The original receiver may have been lost.

6. Is it a normal function call?

fn();

Then check strict mode and the runtime.

This checklist is far more useful than memorizing isolated examples.


The this Cheat Sheet

Situation this
obj.method() obj
fn() in strict mode undefined
fn() in non-strict classic code Global object
fn.call(obj) obj
fn.apply(obj) obj
fn.bind(obj) Bound to obj
new Fn() New object
Arrow function Lexical this
Arrow + call/apply/bind Lexical this remains
Regular DOM listener currentTarget
Arrow DOM listener Lexical this
ES module top-level undefined
Classic browser script top-level Global object

Conclusion

You don't need to memorize dozens of unrelated rules.

Start with this:

                     this
                      │
             ┌────────┴────────┐
             │                 │
       Regular function    Arrow function
             │                 │
             ↓                 ↓
      Check the call      Check outside
             │                 │
      ┌──────┼──────┐          │
      ↓      ↓      ↓          ↓
    obj.     new   call/    surrounding
   method()       apply/      this
                   bind
      │      │       │
      ↓      ↓       ↓
    object  new    explicit
            object   value

And remember the two rules that matter most:

Regular functions get their this from how they are called.

Arrow functions don't have their own this; they inherit it from their surrounding scope.

Once you start reading JavaScript by looking at the function type + call site, this stops looking random.

It becomes predictable.


Quick Recap

object.method()
      ↓
this = object


fn()
      ↓
strict → undefined
non-strict → global object


fn.call(obj)
fn.apply(obj)
fn.bind(obj)
      ↓
this = obj


new Fn()
      ↓
this = new object


arrow function
      ↓
this = surrounding lexical this

The next time you see:

this

don't ask:

"What object is this inside?"

Ask:

"What kind of function is this, and how was it called?"

That question is the key to understanding JavaScript's this.

5 views

More from this blog