The JavaScript `this` Keyword: All 5 Binding Rules in One Place

Link copied
The JavaScript `this` Keyword: All 5 Binding Rules in One Place

The JavaScript `this` Keyword: All 5 Binding Rules in One Place

JS Tutorial Module 2: Functions Lesson 2.6

this is the JavaScript feature that has burned more developer hours than any other. The problem is not that this is complicated — it follows five clear rules — the problem is that almost no resource teaches all five together.

This article does. By the end, you will be able to predict the value of this in any code snippet, including the tricky ones interviewers love.

This is Part 5 of the JavaScript Fundamentals series. The execution context model from Part 1 is the foundation — every execution context has a this binding determined by how the function was called.

The one sentence that explains this #

this is set by how a function is called, not where it is defined.

Memorize that. Once you internalize it, every "weird" this behavior collapses into one of five well-defined cases. Arrow functions are the exception — they inherit this lexically — and we cover them last.

Rule 1: Default binding #

When you call a plain function with no context, this is the global object (in non-strict mode) or undefined (in strict mode).

function whoAmI() { return this; }

whoAmI(); // window / globalThis (non-strict)
          // undefined (strict)

In 2026 almost all code is strict mode (modules and class bodies are strict by default), so the practical answer is: this is undefined in unbound function calls.

Rule 2: Implicit binding (method call) #

When a function is called as a method (obj.fn()), this is the object to the left of the dot:

const user = {
  name: 'Alice',
  greet() { return `Hi, I am ${this.name}`; },
};

user.greet(); // 'Hi, I am Alice' — this === user

The object that owns the method at the moment of the call wins. Watch what happens when you tear the method off:

const greet = user.greet;
greet(); // 'Hi, I am undefined' — this is undefined (default binding)

No dot, no implicit binding. This is why callbacks lose this:

setTimeout(user.greet, 0); // 'Hi, I am undefined' — passed without context

The fix is either explicit binding (rule 3) or an arrow function wrapper (rule 5).

Rule 3: Explicit binding (call, apply, bind) #

Three built-in methods on every function let you set this directly:

function introduce(greeting) { return `${greeting}, I am ${this.name}`; }

const me = { name: 'Sam' };

introduce.call(me, 'Hello');     // 'Hello, I am Sam'
introduce.apply(me, ['Hello']);  // same — apply takes args as an array

const boundIntro = introduce.bind(me);
boundIntro('Hey'); // 'Hey, I am Sam'
boundIntro('Hi');  // 'Hi, I am Sam' — me is permanently bound
  • call(thisArg, ...args) — invokes immediately with thisArg and individual arguments.
  • apply(thisArg, argsArray) — same, but arguments come in an array.
  • bind(thisArg, ...args) — returns a new function with thisArg (and optionally some args) permanently bound.

Explicit binding beats implicit. user.greet.call(otherUser) ignores user and uses otherUser.

Rule 4: new binding (constructor call) #

When you invoke a function with new, four things happen:

  1. A brand-new empty object is created.
  2. The function's prototype is linked to it (more on this in Part 12).
  3. this inside the function is bound to the new object.
  4. If the function does not return its own object, the new object is returned automatically.
function User(name) {
  this.name = name;
}

const u = new User('Bob');
u.name; // 'Bob' — this was the new object

Because new creates the binding, it overrides every other rule. new User.call(other, 'Bob') is a SyntaxError — you cannot mix new with explicit binding.

Rule 5: Arrow functions (lexical this) #

Arrow functions break the entire pattern. They do not have their own this binding. Instead, they capture this from the surrounding lexical scope at the moment they are created:

const user = {
  name: 'Alice',
  greetArrow: () => `Hi, I am ${this.name}`,
  greetMethod() { return `Hi, I am ${this.name}`; },
};

user.greetArrow();  // 'Hi, I am undefined' — arrow captures OUTER this (module scope)
user.greetMethod(); // 'Hi, I am Alice'   — implicit binding (rule 2)

This trips people up: an arrow function as a method does not get the implicit binding. It uses whatever this was in the scope where the arrow was defined.

When arrows save you #

Arrows are perfect for callbacks where you want this to remain the enclosing object:

class Timer {
  constructor() {
    this.seconds = 0;
    setInterval(() => {
      this.seconds++; // arrow captures Timer instance
    }, 1000);
  }
}

If you used a regular function () {} here, this inside would be undefined (rule 1 — setInterval calls callbacks without context).

When arrows hurt you #

Do not use arrow functions:

  • As object methods that need this to be the object.
  • As event handlers when you need this to be the element (addEventListener('click', function() { this; }) gets the element; the arrow version gets the outer scope).
  • With call/apply/bind — those are ignored on arrow functions; the lexical this always wins.

The precedence ladder #

When multiple rules could apply, this is the precedence (highest first):

  1. Arrow function — always lexical this, ignores everything else.
  2. new — creates a new object.
  3. Explicit binding (call / apply / bind).
  4. Implicit binding (obj.fn()).
  5. Default binding — global / undefined.

Always start at the top: "Is this an arrow function?" If yes, find the enclosing scope's this. If no, work down the list.

A worked debug #

Question: what does this log?

const user = {
  name: 'Alice',
  hello() {
    return function () { return this.name; };
  },
};

user.hello()(); // ???

Walk it:

  1. user.hello() — implicit binding (rule 2). Inside hello, this === user.
  2. Returns an anonymous regular function.
  3. We invoke that returned function with (). No object on the left of the call, no call/apply/bind, not a constructor, not an arrow. → Default binding (rule 1).
  4. In strict mode, this is undefinedundefined.name throws TypeError.

Fix with an arrow function:

const user = {
  name: 'Alice',
  hello() {
    return () => this.name; // arrow captures hello's this (= user)
  },
};
user.hello()(); // 'Alice' ✓

Or with bind:

const user = {
  name: 'Alice',
  hello() {
    return function () { return this.name; }.bind(this);
  },
};

this in classes #

Classes are always strict-mode. Method calls on instances bind this to the instance:

class Counter {
  constructor() { this.count = 0; }
  inc() { this.count++; }
}

const c = new Counter();
c.inc(); c.count; // 1

// But:
const inc = c.inc;
inc(); // TypeError — this is undefined

Class methods are not auto-bound. Either define them as arrow function fields:

class Counter {
  count = 0;
  inc = () => { this.count++; };
}

Or bind in the constructor:

class Counter {
  constructor() {
    this.count = 0;
    this.inc = this.inc.bind(this);
  }
  inc() { this.count++; }
}

Arrow-field methods are simplest. The downside is each instance gets its own copy of the function (instead of one shared on the prototype). For most apps the cost is negligible.

this in event handlers #

DOM event handlers attached with addEventListener get this === element:

button.addEventListener('click', function () {
  this; // the button element
});

But not with arrow functions:

button.addEventListener('click', () => {
  this; // outer scope (often undefined in modules)
});

Usually you want event.currentTarget or event.target anyway — it works regardless of function type. Prefer that.

this in modules #

At the top level of an ES module, this is undefined. It is not the module object, not globalThis. This catches transplanted-from-script code:

// my-module.js
console.log(this); // undefined

If you really need the global, use globalThis.

Common gotchas #

Gotcha 1: Losing this in destructured methods.

const { greet } = user;
greet(); // this lost

Destructuring tears the method off the object — same as assigning to a variable.

Gotcha 2: forEach callback loses outer this.

class Group {
  constructor() { this.items = []; this.tag = 'g'; }
  tagAll() {
    this.items.forEach(function (i) {
      i.tag = this.tag; // TypeError — this is undefined here
    });
  }
}

Fix: arrow function callback (inherits this) or pass the second arg to forEach:

this.items.forEach(function (i) { i.tag = this.tag; }, this);

Gotcha 3: setTimeout(this.method, 0) loses this.

setTimeout(this.tick, 1000);          // ✗ this lost
setTimeout(() => this.tick(), 1000);  // ✓ arrow keeps it
setTimeout(this.tick.bind(this), 1000); // ✓ bind

Gotcha 4: bind is permanent. Once bound, a function ignores future call/apply attempts to rebind:

const bound = fn.bind(a);
bound.call(b); // still bound to a

Recap #

Five binding rules, top precedence first:

  1. Arrow function — lexical this (from where it was defined).
  2. new — newly created object.
  3. Explicitcall / apply / bind.
  4. Implicit — object to the left of the dot.
  5. Default — global or undefined (strict).

The one sentence: this is set by how a function is called. Apply the ladder; the answer always pops out.

Next up: Type Coercion and Equality — why [] == ![] is true, when == is genuinely useful, and how falsy/truthy traps cost real money in production.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *