Understanding the this Keyword

Mental model: this is not decided by where a function is written. It is decided by how the function is called.

Level: intermediate · about 15 minutes

function whoAmI() {
  return this;
}

const obj = { name: 'obj', whoAmI };

console.log(obj.whoAmI().name);        // 'obj'
console.log(whoAmI.call({ name: 'x' }).name); // 'x'
console.log(new whoAmI() instanceof whoAmI);  // true
console.log(whoAmI());                 // undefined in strict code, globalThis in sloppy

One function, four call sites, four different values of this.

The function body never changed. Only the call site did. That is the whole lesson: this is an extra, invisible parameter that the call site fills in, and there are exactly four ways it can be filled.

The four rules, in precedence order

PriorityRuleCall looks likethis becomes
1 (highest)new bindingnew Fn()the brand new object
2Explicit bindingfn.call(o), fn.apply(o), fn.bind(o)o
3Implicit bindingo.fn()o (the thing left of the dot)
4 (lowest)Default bindingfn()undefined in strict mode, globalThis in sloppy

Read a call site right to left: is there a new? Then rule 1. No? Is there a .call, .apply or a prior .bind? Rule 2. No? Is there a dot immediately before the call? Rule 3. Nothing at all? Rule 4.

        look at the CALL SITE
                  │
        ┌─ new Fn() ? ──── yes ──► this = the fresh object
        │ no
        ├─ Fn.call / apply / bound ? ── yes ──► this = the given value
        │ no
        ├─ obj.Fn() ? ─── yes ──► this = obj
        │ no
        └─ Fn() ──────────────► this = undefined (strict)
                                       globalThis (sloppy)

  arrow functions are not on this tree at all:
  they have no this of their own, so they use the
  enclosing scope's this, lexically.
  1. Rule 4: default binding A plain call with nothing in front of it. In a module or any strict code, this is undefined. In a sloppy-mode script it is coerced to globalThis, which is the source of a thousand accidental globals.
  2. Rule 3: implicit binding Only the last dot counts. In a.b.c(), this inside c is b, not a.
  3. Rule 2: explicit binding call and apply set this for one call. bind returns a new function with this welded on permanently.
  4. Rule 1: new binding new creates a fresh object and passes it as this. It beats everything else, including a hard bind.

Strict versus sloppy default binding

Sloppy script: coerced to globalThis

function f() {
  return this === globalThis;
}
f(); // true

// this is never null or undefined
// in sloppy mode: primitives get
// boxed, null becomes globalThis.

Strict code and modules: left alone

'use strict';
function f() {
  return this;
}
f();            // undefined
f.call(7);      // 7 (a number, not Number)
f.call(null);   // null

ES modules are always strict, class bodies are always strict, and the exercises on this site run in strict mode. this.something = x in a plain call therefore throws a TypeError instead of quietly creating a global. That is a feature.

Losing the binding

Implicit binding is fragile because it is a property of the call, not of the function. Pull the method out of the object and the dot is gone, so rule 3 no longer applies.

const counter = {
  count: 0,
  increment() { this.count += 1; return this.count; },
};

console.log(counter.increment());     // 1, dot present, this = counter

const detached = counter.increment;   // no call yet, just the function
try {
  detached();                         // rule 4: this is undefined in strict code
} catch (err) {
  console.log(err.constructor.name);  // 'TypeError'
}

This is the same shape as every real-world version of the bug: setTimeout(counter.increment), arr.map(obj.format), element.addEventListener("click", app.handleClick). You passed the function and left the object behind.

FixCodeNotes
Wrap in an arrowsetTimeout(() => counter.increment())The dot survives inside the arrow. Usually the clearest fix.
Bind at the boundarysetTimeout(counter.increment.bind(counter))Explicit, but creates a new function each time you call it.
Bind in the constructorthis.handle = this.handle.bind(this)One bound copy per instance. The pre-arrow React idiom.
Use a class field arrowhandle = () => { ... }Per-instance, auto-bound, but not on the prototype.
Close over the objectconst c = counter; () => c.increment()A closure, not this. No binding to lose.

Arrow functions have no this

An arrow function does not get its own this binding at all. When you write this inside one, the name resolves lexically, exactly like any other variable: outward through the enclosing scopes until some scope has a this. call, apply and bind cannot change it.

const timer = {
  label: 'build',
  startBad: function () {
    setTimeout(function () {
      console.log('bad:', this?.label);   // undefined: rule 4 inside the callback
    }, 0);
  },
  startGood: function () {
    setTimeout(() => {
      console.log('good:', this.label);   // 'build': this came from startGood
    }, 0);
  },
};

timer.startBad();
timer.startGood();

Broken: arrow as a method

const user = {
  name: 'Ada',
  hello: () => `Hi ${this?.name}`,
};

user.hello(); // 'Hi undefined'
// The dot cannot help: arrows
// ignore the call site.

Correct: shorthand method

const user = {
  name: 'Ada',
  hello() { return `Hi ${this.name}`; },
};

user.hello(); // 'Hi Ada'
// Rule 3 applies, because
// hello has its own this.

Rule of thumb: an arrow is right when you WANT to inherit this (a callback inside a method). It is wrong when you want the call site to provide this (a method, a prototype method, an event handler that needs event.currentTarget).

const app = {
  name: 'lab',
  run() {
    const inner = function () { return this?.name; };
    const arrow = () => this?.name;
    return [inner(), arrow()];
  },
};
console.log(app.run());

inner() is a plain call, so rule 4 applies and this is undefined (module code is strict), giving undefined. arrow has no this of its own, so it uses run’s this, which rule 3 set to app. Nesting does not preserve this for normal functions; only arrows inherit it.

Two more edges worth knowing

const api = { base: '/v1', url(path) { return this.base + path; } };

const { url } = api;                 // binding lost right here
console.log(api.url('/users'));      // '/v1/users'
console.log(typeof url);             // 'function', but it has no home

const paths = ['/a', '/b'];
console.log(paths.map(api.url.bind(api))); // ['/v1/a', '/v1/b']

Destructuring a method and passing a method reference are the same mistake.

`this` in a method
the object left of the last dot
`this` in a plain call
undefined (strict) or globalThis (sloppy)
`this` in an arrow
whatever the enclosing scope had, lexically
`this` in a class body
always strict, so never globalThis
`this` in a getter
the object the property was read from
`this` at module top level
undefined

A function does not own its this. The call site lends it one.

The shortest correct summary of this lesson

Try it yourself

Exercise the four rules

function report(prefix = '') {
  return `${prefix}this.name = ${this?.name}`;
}

const a = { name: 'A', report };
const b = { name: 'B' };

console.log(a.report('implicit: '));        // rule 3
console.log(report.call(b, 'explicit: '));  // rule 2
console.log(report('default:  '));          // rule 4
console.log(new report('new:      '));      // rule 1 returns the object

Predict each line, then run. Now add a fifth call site using bind, and one using an arrow wrapper.

The arrow method trap

const widget = {
  id: 'w1',
  bad: () => `bad: ${this?.id}`,
  good() { return `good: ${this.id}`; },
  delayed() {
    return [
      function () { return this?.id; },   // rule 4
      () => this.id,                      // lexical
    ].map((f) => f());
  },
};

console.log(widget.bad());
console.log(widget.good());
console.log(widget.delayed());

Fix bad without touching the call site. Then break good by turning it into an arrow.

Exercises

Build a timer that cannot lose its binding

Write makeTimer(label). It returns an object with a label, a ticks count starting at 0, and a tick() method that increments ticks and returns the string "label: n". The catch: tick must keep working when it is detached from the object, for example const t = makeTimer("build"); const fn = t.tick; fn();. Two timers must not share a count.

Check yourself

What does this log?
['own', 'own', 'function'] — o.read() uses implicit binding so it returns "own". bound() was hard-bound to o, so it also returns "own". read is never called, so nothing throws: typeof read is just "function". Calling read() in strict code would be the TypeError.
Which call site wins when several rules could apply?
new beats explicit, which beats implicit, which beats default — The precedence is fixed: new > explicit (call/apply/bind) > implicit (obj.fn()) > default (fn()). That is why new (Fn.bind(other))() still gets the fresh object rather than other.
Why can call not change this inside an arrow function?
Arrows have no this binding of their own, so this is resolved lexically like any variable — An arrow function never creates a this binding. The this you write inside it belongs to the enclosing scope, so call, apply and bind have nothing to overwrite. They still pass arguments, they just cannot change this.
In a plain fn() call inside an ES module, what is this?
undefined — Modules are always strict, so default binding leaves this as undefined instead of coercing it to the global object. This is why this.x = 1 in a stray helper throws a TypeError in a module but silently creates a global in an old script.

Common mistakes

  • Assuming this follows the same lexical rules as variables. It does not, except in arrows.
  • Writing an arrow function as an object method, then wondering why this is undefined.
  • Passing obj.method as a callback and losing the object.
  • Thinking this in a.b.c() is a. Only the last dot counts.
  • Testing this behaviour in a sloppy-mode script and being surprised in a module, where undefined is not coerced.

Takeaways

  • this is bound at call time, not at definition time.
  • Four rules, in order: new, explicit, implicit, default.
  • Default binding is undefined in strict code and globalThis in sloppy code.
  • Arrow functions have no this; they inherit the enclosing one lexically and cannot be rebound.
  • Passing a method somewhere else drops the dot, and with it the binding.