Skip to content

3.6.3 — The this Keyword, Completely

Every binding in JavaScript obeys the lexical rule from 3.6.1: look where the code is written. Every binding except one. this is resolved by how a function is called — dynamically, at the call site, fresh on every invocation. That single inconsistency has produced more confusion than any other feature of the language, and more interview questions than almost any other topic. Explain the this keyword. [EQ-570]

The cure is the same as for closures: not memorizing outcomes but owning the rule set. There are exactly five binding rules, a strict precedence order among them, and a handful of consequences. From those, every this puzzle — lost methods, broken callbacks, arrow-vs-regular, bind chains, class handlers — becomes derivable. This page builds all of it.

1. What this actually is

this is an implicit parameter. Every regular function call receives it, exactly as if every function had a hidden zeroth argument:

javascript
function greet(punctuation) {
  return `Hello, I am ${this.name}${punctuation}`;
  //                    └── reads the hidden parameter, set BY THE CALL
}

The function's text never determines this — the call site does. Same function, different callers, different this:

javascript
const ada   = { name: "Ada",   greet };   // same function object attached
const linus = { name: "Linus", greet };   // to two objects

ada.greet("!");     // → "Hello, I am Ada!"
linus.greet("?");   // → "Hello, I am Linus?"
const f = ada.greet;
f("…");             // → TypeError (strict): this is undefined

Why design it this way? Because JavaScript methods are just function-valued properties — the language needed a way for one shared function to serve many objects (3.6.4 shows this is how all prototype methods work: thousands of instances, one function, this telling it which instance to operate on). Dynamic this is what makes shared methods possible; the pain is the price.

2. The five binding rules

Every call in JavaScript matches exactly one of these. Learn them as questions you ask about the call site, in this order.

Rule 1 — new binding: is it called with new?

javascript
function User(name) {
  // `new` did four things before this line runs:
  //  (1) created a fresh empty object
  //  (2) linked it to User.prototype        (see 3.6.4)
  //  (3) bound `this` to it
  this.name = name;   // ← so this property lands on the new object
  //  (4) returns `this` automatically (unless you return an object explicitly)
}
const u = new User("Ada");   // → { name: "Ada" }

new Fn(…) constructs: this is the brand-new object. (The four steps above are the complete, precise answer to "what does new do?" — an interview staple.)

Rule 2 — explicit binding: is it called via call, apply, or bind?

The three methods every function inherits let you set this yourself:

javascript
function greet(greeting, punct) { return `${greeting}, ${this.name}${punct}`; }
const ada = { name: "Ada" };

greet.call(ada, "Hi", "!");        // → "Hi, Ada!"     args listed one by one
greet.apply(ada, ["Hi", "!"]);     // → "Hi, Ada!"     args as an Array
const bound = greet.bind(ada);     // returns a NEW function, this locked forever
bound("Hey", "?");                 // → "Hey, Ada?"    call it whenever, however
  • call invokes immediately, arguments comma-separated.
  • apply invokes immediately, arguments as an array. (Since ES6, fn(...args) spread has replaced most apply uses.)
  • bind does not invoke — it manufactures a new function with this (and optionally leading arguments) permanently fixed. Two properties matter: permanence — a bound function's this cannot be re-bound, not even by call; and partial applicationconst log = console.log.bind(console, "[auth]") pre-fills arguments, the closure-style factory of 3.6.2 done with bind.

Rule 3 — implicit binding: is it called through an object — obj.fn()?

this is the object before the dot — and only the immediate one:

javascript
const app = { name: "app", ui: { name: "ui", who() { return this.name; } } };
app.ui.who();   // → "ui"  — the LAST dot wins, not the first

Rule 4 — default binding: a bare call — fn()?

No new, no explicit binding, nothing before the dot. Then:

  • Strict mode (3.6.1): this is undefined. Touching this.name throws — loud and early.
  • Sloppy mode: this is the global object (window in browsers, globalThis generally) — the legacy behavior that quietly turned bugs into global-variable pollution: this.name = "x" in a sloppy plain call creates a global. This is one of strict mode's main reasons to exist.

Rule 5 — arrow functions: no this at all

An arrow function does not bind this differently — it doesn't have one. Using this inside an arrow resolves it like any ordinary variable: outward through the scope chain, landing on the enclosing regular function's this (or the module/global's). It is the one place this obeys lexical scoping.

javascript
const timer = {
  seconds: 0,
  start() {                                // regular method: this = timer (Rule 3)
    setInterval(() => {
      this.seconds++;                      // arrow: no own this → uses start's this ✅
    }, 1000);
  }
};

Because the arrow's this is settled by position in the code, call/apply/bind cannot change it, and arrows cannot be used with new.

Precedence — when rules collide

A call site can look like several rules at once. The order is strict:

new > explicit (bind/call/apply) > implicit (obj.fn()) > default (fn()) — and arrows opt out of the whole system.

javascript
const obj = { name: "obj", f: greet.bind({ name: "bound" }) };
obj.f("Hi", "!");        // → "Hi, bound!"   explicit beats implicit
Is the function an ARROW?yes → lexical: enclosing scope's this① Called with new?yes → the freshly created object② call / apply / bind?yes → the object you passed③ Called as obj.fn()?yes → object before the (last) dot④ Bare call fn()strict: undefined · sloppy: globalThisno
Figure 1 — Resolving this at any call site. Ask the questions top-down; the first "yes" wins. Arrows short-circuit the whole ladder — they simply read the enclosing scope's this like a normal variable.

3. The lost-this bug family

Nearly every real-world this bug is the same event: a method travels away from its object, then gets invoked as a bare function (Rule 4). Three disguises:

Extraction. Assigning a method to a variable strips the object:

javascript
const user = { name: "Ada", greet() { return `Hi ${this.name}`; } };
const g = user.greet;    // just the function object — the `user.` is GONE
g();                     // → TypeError: this is undefined

Passing as a callback. The same thing, hidden inside an API — you never see the bare call, but it happens:

javascript
setTimeout(user.greet, 100);          // [!code error] // timer calls it bare → this lost
button.addEventListener("click", user.greet);   // [!code error] // same disease

setTimeout(user.greet, …) passes only the function; when the timer fires it calls fn() — Rule 4. The property access and the call were separated in time, and only call-time matters.

Array-method and framework callbacks. arr.map(obj.method), promise chains .then(obj.method), event buses — all invoke your function bare.

The fixes (all equivalent — pick per context):

javascript
setTimeout(() => user.greet(), 100);        // (a) wrap: the arrow performs a real
                                            //     obj.method() call at fire time
setTimeout(user.greet.bind(user), 100);     // (b) bind: lock this permanently
const g2 = (...a) => user.greet(...a);      // (c) extraction-safe wrapper

A fourth fix exists inside classes — worth its own section, because React made it famous.

4. this in classes — and the class-field arrow idiom

class methods (3.6.4) live on the prototype and follow the same five rules — including the lost-this trap when used as handlers:

javascript
class Counter {
  count = 0;
  increment() { this.count++; }                 // prototype method — losable
  incrementSafe = () => { this.count++; };      // [!code highlight] // class FIELD holding an ARROW
}
const c = new Counter();
button.addEventListener("click", c.increment);      // [!code error] // broken: bare call
button.addEventListener("click", c.incrementSafe);  // works forever ✅

Why does the field version work? Unpack the syntax: a class field initializer runs during construction, in a scope where this is the instance being built (Rule 1). The initializer creates an arrow function, which has no own this — so it lexically captures the constructor-time this, i.e. this exact instance, permanently (3.6.2: it's a closure over the construction scope). You've effectively written this.incrementSafe = (…) => … in the constructor — one bound-forever function per instance.

Trade-off to know at the expert level: prototype methods are allocated once and shared by all instances; class-field arrows are allocated per instance (N instances → N function objects) and, being own properties, aren't shared, mockable-via-prototype, or visible to super. For a handful of UI components: irrelevant. For thousands of instances: measurable. The pre-fields idiom this.increment = this.increment.bind(this) in the constructor has the identical per-instance cost — class-field arrows are its modern spelling.

5. The remaining corners — a complete sweep

Top level. In a script, top-level this is globalThis (window); in an ES module (3.6.5), top-level this is undefined — modules are always strict. globalThis (ES2020) is the standardized "the global object, wherever I run" — browser window, Node global, workers' self — write it instead of any of those.

DOM event handlers. When you attach a regular function with addEventListener, the browser calls it with this = the element the listener is attached to (event.currentTarget). Attach an arrow and this is whatever the enclosing scope says — usually not the element. Prefer reading event.currentTarget explicitly; it's immune to the choice of function kind.

Callbacks with a thisArg. Several built-ins accept an optional final thisArg parameter that they'll use when invoking your callback: arr.map(fn, thisArg), forEach, filter, find, Set/Map.forEach. A leftover convenience from before arrows; today an arrow callback is clearer.

Getters/setters. Inside a getter (3.6.4), this is the object the property was accessed through — implicit binding applies, which is what makes getters work correctly even when inherited via the prototype chain.

bind + new (the dark corner). Calling new on a bound function ignores the bound this (construction wins — precedence rule 1) but keeps the bound arguments. You will likely never write this; it exists so partially-applied constructors work.

Sloppy-mode boxing. In sloppy mode, call/apply with a primitive this (fn.call(42)) boxes it into a Number object, and null/undefined are silently replaced with the global object. Strict mode passes all of them through untouched — one more reason strict semantics are saner.

6. The expert lens

this is a hidden parameter — say it that way and the mystery evaporates. "Which object is this method operating on?" is data the call site must supply, exactly like any argument. Languages differ only in spelling: Python makes it explicit (def method(self, …)), Rust likewise (fn method(&self)), JavaScript passes it invisibly. When mentoring, translating user.greet() to "greet(user) with sugar" fixes most confusion in one sentence.

Two binding systems, one language. Everything else in JavaScript is lexical; this alone is dynamic. Arrow functions (2015) were the language formally conceding that most callbacks want lexical this — twenty years of var self = this; at the top of functions was the community screaming it. Modern style has converged: regular functions/methods when you need dynamic this (shared methods, DOM handlers reading the element), arrows for everything that merely uses the surrounding context (callbacks, class-field handlers). If you find yourself writing .bind(this) today, an arrow probably wants to exist there instead.

Design lesson. Dynamic this bought method sharing at the cost of a rule that breaks the language's own grand principle (lexical resolution). The industry verdict is visible in newer languages and APIs: explicit receivers, or no this at all (React hooks abandoned classes largely to escape this-binding bugs). When you design an API, beware any feature whose meaning silently changes with how it's invoked — implicit context is the hardest kind of coupling to debug.

Next: the object system that this exists to serve — 3.6.4: property descriptors, the prototype chain, and what class really compiles down to.

Recall

  • this is an implicit parameter set by the call site, not by where the function is written — the one dynamic binding in a lexically-scoped language.
  • Five rules, asked in order: new (fresh object; new creates → links prototype → binds → returns), explicit call/apply/bind (you choose; bind returns a new permanently-locked function and can pre-fill arguments), implicit obj.fn() (object before the last dot), default bare call (undefined in strict mode, globalThis sloppy), and arrow functions — which have no own this and read the enclosing scope's, immune to call/bind/new.
  • Precedence: new > explicit > implicit > default. The lost-this family (extraction, setTimeout(user.greet), callback passing) is always "method traveled, then invoked bare"; fix with an arrow wrapper or bind.
  • Class-field arrows (handle = () => {…}) capture construction-time this per instance — the modern auto-bind idiom; cost: one function object per instance versus one shared prototype method.
  • Corners: module top-level this is undefined; DOM listeners set this to the element (prefer event.currentTarget); map-style thisArg params are legacy; globalThis is the portable global.

Self-test: Recite the four things new does. Why does setTimeout(user.greet, 100) fail while setTimeout(() => user.greet(), 100) works? What exactly makes a class-field arrow "auto-bound," and what does it cost? Which wins: bind or implicit? Arrow or call? What is top-level this in an ES module?

Quiz Bank

FoundationalWhat determines the value of this in a regular function?

The call site — how the function is invoked, at each invocation. this is best understood as an implicit parameter passed by the call. Four call shapes set it: new Fn() → the newly created object; fn.call/apply/bind(x)x (explicit); obj.fn()obj, the object before the last dot (implicit); bare fn()undefined in strict mode, the global object in sloppy mode (default). Precedence when shapes combine: new > explicit > implicit > default. Arrow functions sit outside the system entirely: they have no own this and resolve it lexically through the scope chain like an ordinary variable — which is why call/bind cannot change an arrow's this and new rejects arrows.

FoundationalWhat exactly does the new operator do?

Four steps, in order: (1) create a fresh empty object; (2) link that object's internal prototype to the function's .prototype property (3.6.4) — this is how instances find their shared methods; (3) invoke the function with this bound to the new object, so this.x = … assignments build the instance; (4) return the new object automatically — unless the function explicitly returns some other object, which then wins (returning a primitive is ignored). Knowing the four steps also explains constructor edge cases: why forgetting new (in sloppy mode) sprays properties onto the global object, and why class constructors throw if called without new.

InterviewDifference between call, apply, and bind?

All three set this explicitly. call invokes immediately with arguments listed individually: fn.call(ctx, a, b). apply invokes immediately with arguments in an array: fn.apply(ctx, [a, b]) — mnemonic: apply = array (largely superseded by spread: fn(...args)). bind does not invoke: it returns a new function with this permanently locked — un-overridable by later call/apply or by implicit binding — and can also pre-fill leading arguments (partial application): const log = console.log.bind(console, "[auth]"). Use call/apply for one-off invocation with a chosen receiver; use bind when the function will be handed elsewhere (callbacks, handlers) and must keep its this when invoked bare later.

InterviewWhy does setTimeout(user.greet, 100) print undefined, and what are the fixes?

Because property access and invocation are separated in time, and only invocation binds this. user.greet evaluates to the bare function object — the user. context is not stored in it. 100 ms later the timer invokes it as a plain call, fn()default binding — so this is undefined (strict) or the global object (sloppy), and this.name fails. This is the lost-this pattern; the same failure hides in addEventListener("click", user.greet), arr.map(obj.method), and .then(obj.method). Fixes: (a) wrap in an arrow — setTimeout(() => user.greet(), 100) — so a genuine user.greet() (implicit binding) happens at fire time; (b) setTimeout(user.greet.bind(user), 100) — lock the receiver permanently; (c) inside classes, declare the handler as a class-field arrow so it's born bound. All three ensure the eventual call site carries the right receiver.

AppliedExplain precisely why a class field holding an arrow function solves the handler-binding problem, and its cost.
javascript
class Form { handle = () => this.submit(); }

A class field initializer executes during construction, when this is the instance being built (the new binding). The initializer produces an arrow function, which has no own this; it lexically captures the construction scope's this — the instance — as a closure (3.6.2). The result is an own property per instance holding a function whose this can never be lost: pass it to addEventListener, setTimeout, anywhere — a later bare call still resolves this lexically to the instance. It is the modern spelling of the constructor idiom this.handle = this.handle.bind(this). Costs: one function object per instance (a prototype method is one object shared by all), it's an own property so it shadows and isn't overridable/mocked via the prototype, and super.handle can't reach it. Fine for typical UI component counts; consider prototype methods + explicit binding for very large instance populations.

StaffA code review shows a utilities object whose methods are destructured for convenience: const { format } = formatter, then format() is called. Sometimes it works, sometimes it crashes. Assess the design and give team guidance.

The crashes are lost this: destructuring is method extractionformat becomes a bare function, and calling it uses default binding (undefined in modules/strict), so any internal this.locale-style access throws. "Sometimes works" means some methods never touch this — the API is silently split into extraction-safe and extraction-unsafe halves, which is a design smell: the object's contract doesn't say which is which.

Guidance ladder: (1) If the utilities are stateless, make them not use this at all — plain functions in a module (3.6.5) are the right shape; destructuring imports is then always safe. This is the best fix: don't fight this, remove it. (2) If shared state is genuine (a configured locale), make the factory pattern explicit — makeFormatter(locale) returning closures over the config (3.6.2); closures can't lose their environment. (3) If it must remain an object with dynamic this, either declare methods as bound (field arrows / constructor bind) or document "never detach" and add an ESLint rule (@typescript-eslint/unbound-method catches exactly this). The senior framing:

dynamic this is an implicit dependency on the call site; APIs handed to unknown call sites (destructuring, callbacks) must either not depend on this or carry the binding with them.

Flashcards

FlashThe five binding rules, ranked

new > call/apply/bind > obj.fn() implicit > bare-call default (strict: undefined). Arrows: no own this — lexical, immune to all four.

FlashWhat new does (4 steps)

Create object → link to Fn.prototype → call with this = object → auto-return it (explicit object return overrides).

Flashcall vs apply vs bind

call: invoke now, comma args. apply: invoke now, array args. bind: don't invoke — return new function, this locked forever, args pre-fillable.

FlashLost-this pattern

Method extracted or passed as callback → later invoked bare → default binding. Fix: arrow wrapper, bind, or class-field arrow.

FlashArrow function this

Doesn't exist — resolved lexically through the scope chain like a normal variable. call/bind can't change it; new is an error.

FlashTop-level this

Script: globalThis. ES module: undefined (always strict). Portable global object: globalThis (ES2020).

Scenario Drill

DrillAfter a refactor moving an app from classes to plain objects, a production error spike shows TypeError: Cannot read properties of undefined (reading 'config') — but only in code paths that go through the analytics queue. Walk the diagnosis and the fix options.

The error says some function is executing with this === undefined and then touching this.config — the classic lost-this signature, and "only through the analytics queue" is the tell: queues, emitters, and schedulers store bare function references and invoke them later as plain calls (default binding, undefined in strict/module code).

Diagnosis walk: find where handlers are registered with the queue — you'll find something like queue.subscribe(tracker.onEvent). At registration time tracker.onEvent evaluates to the function object alone; the queue later fires handler(payload) — a bare call — so inside onEvent, this is undefined and this.config explodes. It worked before the refactor because the class version happened to register with a bound method (or the framework auto-bound); the object version dropped that. Confirm cheaply: log this at the top of onEvent, or set a breakpoint and inspect — no guessing.

Fix options, best first: (1) Remove the dependency on this — if onEvent only needs config, refactor to a closure factory: makeTracker(config) returns onEvent closing over config (3.6.2); closures cannot lose their environment, so registration is unconditionally safe. (2) Register a wrapper: queue.subscribe((e) => tracker.onEvent(e)) — the arrow performs a genuine implicit-binding call at fire time. (3) Bind at registration: queue.subscribe(tracker.onEvent.bind(tracker)) — also required if you ever need to unsubscribe (note: bind returns a new function each call, so store the bound reference; re-binding at unsubscribe time removes nothing).

Prevent recurrence: lint with unbound-method, and adopt the team rule from this chapter — anything handed to an external invoker must either not use this or carry its binding with it.