Engineering Notes
Home/Languages/JavaScript/The this Keyword
Languages · JavaScript

The this Keyword

this is not fixed by where a function is defined — it is determined by how the function is called. The same function can have a different this on every call. There are four binding rules, checked in priority order.

💻 Languages● Beginner⏱ 7 min read🛠️ 4 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • The Core Idea
  • Rule 1 — Default Binding (plain function call)
  • Rule 2 — Implicit Binding (method call)
  • Rule 3 — Explicit Binding (call, apply, bind)
  • Rule 4 — new Binding (constructor call)
Part 1Binding Rules

1The Core Idea

this is not fixed by where a function is defined — it is determined by how the function is called. The same function can have a different this on every call. There are four binding rules, checked in priority order.

2Rule 1 — Default Binding (plain function call)

js
function whoAmI() {
  console.log(this);
}
whoAmI();
// non-strict mode: globalThis (window / global)
// strict mode ("use strict"): undefined

Calling a function with no receiver (fn(), not obj.fn()) triggers default binding. Always assume strict mode in modern code (ES modules are strict by default) — this will be undefined, not the global object.

3Rule 2 — Implicit Binding (method call)

js
const user = {
  name: "Ana",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};
user.greet(); // "Hi, I'm Ana" — this === user

Whatever object is immediately to the left of the dot at the call site becomes this. This is purely about the call site, not where the function was defined.

js
const greetFn = user.greet;
greetFn(); // "Hi, I'm undefined" — this === undefined (strict) — lost its receiver!

This is the single most common this bug: extracting a method as a standalone reference loses implicit binding.

4Rule 3 — Explicit Binding (call, apply, bind)

js
function greet() {
  console.log(`Hi, I'm ${this.name}`);
}
const person = { name: "Leo" };

greet.call(person);           // "Hi, I'm Leo" — invoke now, this = person
greet.apply(person);          // same as call, but arguments passed as an array
const bound = greet.bind(person);
bound();                       // "Hi, I'm Leo" — this permanently fixed to person
MethodInvokes immediately?Argumentsthis
.call(thisArg, a, b, ...)✅ yescomma-separatedset to thisArg
.apply(thisArg, [a, b])✅ yesarrayset to thisArg
.bind(thisArg, a, b, ...)❌ no — returns a new functionpre-filled, then more allowed at call timepermanently set to thisArg

bind creates a new function whose this cannot be overridden later, even by implicit binding.

5Rule 4 — new Binding (constructor call)

js
function User(name) {
  this.name = name;
}
const u = new User("Mia");
console.log(u.name); // "Mia"

new creates a brand-new empty object, sets this to that object inside the function, and (if the function doesn't explicitly return an object) returns it automatically.

6Priority Order

When multiple rules could apply, this order wins:

text
new binding  >  explicit binding (call/apply/bind)  >  implicit binding (obj.method())  >  default binding (plain call)
Priority order: new binding beats explicit binding beats implicit binding beats default binding
Priority order: new binding beats explicit binding beats implicit binding beats default binding

A function created with bind() cannot have its this changed by new in most engines' practical usage patterns for regular functions — but bind-produced functions can still be used as constructors in edge cases; this is rarely relied upon in real code.

Under the Hoodthis as a Hidden Call Argument▾

this isn't resolved by searching a scope chain the way a normal variable is — it's set up fresh on every single function invocation, as part of building that call's execution context. Mechanically, engines pass the receiver as an extra, hidden argument alongside the explicit ones — the calling convention itself carries this, not the variable-lookup machinery from the Scope topic.

text
A call like  user.greet()   compiles to roughly:
  CallProperty  greet_function, receiver=user, args=[]
                                  ^^^^^^^^^^^^
                          `this` travels as part of the CALL instruction itself,
                          not as a value looked up inside greet()'s body

This is why the four binding rules are checked at the call site, not inside the function: the bytecode for calling a function differs depending on whether it's a bare call (fn()), a property call (obj.fn()), or a new call — each emits a different instruction that supplies (or omits) the receiver differently, before greet's body ever starts running.

Derived class constructors: this literally does not exist yet

The spec formalizes this with an internal [[ThisBindingStatus]] slot on every execution context:

Function kind[[ThisBindingStatus]]Meaning
Regular function / base class constructorinitializedthis is usable immediately
Arrow functionlexicalno own this at all — inherited from the enclosing context (full detail in Part 2)
Derived class constructor (extends)uninitializedthis doesn't exist until super() runs
js
class Dog extends Animal {
  constructor(name) {
    console.log(this); // ReferenceError: must call super constructor before accessing 'this'
    super(name);          // THIS call is what actually creates `this` and flips the status to initialized
  }
}

This is a genuinely different runtime state from a plain TDZ (Variables and Hoisting topics) — this in a derived constructor isn't just "uninitialized," it's a slot that the super() call itself populates, by receiving the object constructed further up the inheritance chain.

Part 2Arrow Functions and Pitfalls

1Arrow Functions Don't Have Their Own this

Arrow functions have no this binding of their own. Instead, they capture this lexically from the enclosing scope at the time they are defined — exactly like a normal variable would be captured by a closure. None of the four binding rules from Part 1 apply to them.

js
const user = {
  name: "Ana",
  greetRegular: function () {
    console.log(this.name); // "Ana" — implicit binding, this = user
  },
  greetArrow: () => {
    console.log(this.name); // undefined — this comes from the OUTER scope, not user
  },
};
user.greetRegular();
user.greetArrow();

2Why This Matters for Callbacks

Arrow functions are the standard fix for the classic "lost this" bug in callbacks:

js
class Timer {
  constructor() {
    this.seconds = 0;
  }
  startBroken() {
    setInterval(function () {
      this.seconds++; // this is undefined/global here — NOT the Timer instance
    }, 1000);
  }
  startFixed() {
    setInterval(() => {
      this.seconds++; // arrow captures `this` from startFixed(), which is the instance
    }, 1000);
  }
}

setInterval calls its callback as a plain function call (default binding), so a regular function loses the instance. An arrow function captures this from startFixed itself, where implicit binding already set it correctly.

3call/apply/bind Do Nothing to Arrow Functions

js
const arrow = () => console.log(this);
arrow.call({ name: "ignored" }); // this is still whatever it was lexically — unaffected

Because arrows never have their own this, none of the explicit-binding methods can change it.

4Arrow Functions Cannot Be Constructors

js
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor

5this in Class Methods

Regular class methods behave like object methods — this depends on the call site:

js
class Counter {
  count = 0;
  increment() { this.count++; }
}
const c = new Counter();
const detached = c.increment;
// detached();  // TypeError-ish: this.count fails, this is undefined (strict mode, class bodies are strict)

Class field arrow functions solve this by permanently binding this to the instance:

js
class Counter {
  count = 0;
  increment = () => { this.count++; }; // arrow class field — this always === instance
}
const c = new Counter();
const detached = c.increment;
detached(); // works — this is bound to c, regardless of how it's called

Tradeoff: each instance gets its own copy of increment (not shared on the prototype), costing a bit more memory per instance versus a regular method.

6Decision Table

SituationUse
Object method that reads this from its receiverregular function/method shorthand
Callback passed to setTimeout/setInterval/array methods, needing the outer thisarrow function
Need this permanently fixed regardless of call site.bind() or an arrow class field
Need a constructor (new)regular function or class — never an arrow

⚠️ Common Pitfalls

  1. Detaching a method (const fn = obj.method; fn();) — loses implicit binding. Fix with .bind(obj) or call as obj.method().
  2. Using an arrow function as an object literal method expecting this to be the object — it won't be; arrows ignore implicit binding entirely.
  3. Forgetting bind returns a new function — fn.bind(obj) does not mutate fn; you must use the returned value.
  4. Assuming class methods auto-bind — only class-field arrow functions do; regular class methods behave like any other function regarding this.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How is this determined?core▾

By how a function is called: method call obj.f() → obj; plain call f() → undefined in strict mode (global object otherwise); new F() → the new object; call/apply/bind → explicitly set; arrow functions capture this from the enclosing scope.

Follow-ups interviewers ask
  • Order of precedence? new > explicit bind/call > method call > default.
Q2Why do arrow functions behave differently with this?core▾

They don't have their own this (or arguments); they use the lexical this of the surrounding code, which makes them ideal for callbacks inside methods but unsuitable as object methods needing dynamic this.

Follow-ups interviewers ask
  • Can you bind an arrow function? No; bind has no effect on its this.
Q3How do you lose this and how do you fix it?core▾

Passing a method as a callback (setTimeout(obj.method)) detaches it from obj. Fix with obj.method.bind(obj), an arrow wrapper () => obj.method(), or class field arrow functions.

Follow-ups interviewers ask
  • Class field arrow trade-off? A new function per instance, not on the prototype.
Q4call vs apply vs bind?core▾

call(thisArg, a, b) and apply(thisArg, [a, b]) invoke immediately with a given this; bind(thisArg, ...args) returns a new function permanently bound (with optional partial arguments).

Follow-ups interviewers ask
  • Can a bound function be rebound? No.
Q5What is this at the top level of an ES module?senior▾

undefined, because modules run in strict mode; in classic scripts it's the global object.

Follow-ups interviewers ask
  • In Node CommonJS? module.exports.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.