gap·map

this & binding

[ junior depth ]

this is decided when the function is called

this is not attached to a function when you write it. The same function gets a different this depending on how it is invoked:

const user = {
  name: "Ann",
  hi() { console.log(`hi, ${this.name}`); },
};
user.hi();          // "hi, Ann" — called through the object
const hi = user.hi;
hi();               // TypeError (strict mode) — same function, no object

The basic rules

  1. Method call. obj.method(): this is the object before the dot.
  2. Plain call. fn(): this is undefined in strict mode (the global object in old sloppy code).
  3. Constructor. new Fn(): this is the brand-new object being built.
  4. Explicit. fn.call(obj) and fn.apply(obj) run the function with this = obj; fn.bind(obj) returns a new function permanently tied to obj.

Arrow functions don't have this at all

An arrow function never gets its own this. It reads the this of the scope where it was written, the same way it reads any other outer variable:

const timer = {
  seconds: 0,
  start() {
    setInterval(() => this.seconds++, 1000); // arrow: this = timer ✓
  },
};

This is why arrows work well as callbacks inside methods, and why an arrow used as the method itself (bye: () => this.name) never sees the object.

The classic bug: losing this

Passing a method somewhere as a bare function drops the object. Interviewers like to hide this bug inside callback code:

setTimeout(user.hi, 500);            // ❌ this is lost
setTimeout(() => user.hi(), 500);    // ✓ wrapper keeps the dot
setTimeout(user.hi.bind(user), 500); // ✓ bind fixes it permanently

The broken line reads user.hi off the object immediately and hands the bare function to the timer. When the timer fires, that is a plain call: this is undefined in strict mode and reading this.name throws.

[ middle depth ]

The rules have a priority order

When bindings collide, the resolution is fixed: new > explicit (bind/call/apply) > implicit (dot call) > default. Arrows sit outside the system entirely; they have no this to bind:

function f() { return this.a; }
const g = f.bind({ a: 1 });
g.call({ a: 2 });   // 1 — call can't override bind
g.bind({ a: 2 })(); // 1 — bind is one-shot; the first bind wins forever

A common wrong answer is "call on a bound function re-targets it". It doesn't: bind returns a new function with this frozen, and later call/apply/bind are ignored (only new can still take over). .bind(obj) on an arrow silently does nothing for this.

Wrapper vs bind: which fix to use

Both standard fixes for a lost this have a failure mode:

  • () => user.hi() reads user late, at call time. If the variable is reassigned in between, you call the new object's method.
  • user.hi.bind(user) freezes the pair now. Reassignment can't affect it, but you allocate a new function, and that new function is the one you have to pass to removeEventListener later.

Where arrows go wrong as methods

const obj = {
  x: 1,
  bad: () => this.x,   // ❌ this = module/global scope, NOT obj
  good() { return this.x; },
};

An arrow written as an object-literal method never sees the object, because the only enclosing scope it can inherit from is the module itself. The working rule: methods get shorthand functions, callbacks inside methods get arrows. Arrows also lack more than this: no arguments, no prototype, and new on an arrow throws.

Classes make the bug loud

Class methods live on the prototype and are not auto-bound. Extracting one detaches it, and since class bodies are always strict mode, it fails with a TypeError instead of silently touching globals:

class Btn {
  label = "ok";
  onProto() { console.log(this.label); }
  onField = () => console.log(this.label); // per-instance, survives extraction
}
const { onProto, onField } = new Btn();
onField(); // "ok"
onProto(); // TypeError

Two idioms repair it: this.onProto = this.onProto.bind(this) in the constructor, or an arrow class field like onField above. Neither is free. The constructor version costs a line of ceremony per method; the field version allocates a fresh function for every instance instead of one shared method on the prototype.

[ senior depth ]

this is a hidden parameter, and sloppy mode rewrites it

Think of this as an implicit argument passed at the call site. In strict mode it arrives untouched: a plain call passes undefined, call(null) passes null, call(42) passes 42. In sloppy mode the engine applies this-substitution: null/undefined become globalThis, and primitives get boxed into wrapper objects, so call(42) arrives as a Number object. Classes never see any of this, since class bodies are always strict.

Top-level this is its own trap: globalThis in a classic <script> (strict or not), but undefined in an ES module. Code migrated from scripts to modules breaks on exactly this line.

Resolution follows the reference, not the prototype chain

this is set by the reference used in the call; where the method lives is irrelevant:

const parent = { greet() { return this.name; } };
const child = Object.create(parent);
child.name = "kid";
child.greet(); // "kid" — found on parent, called on child

Interviewers reach for a handful of spec corners to separate bands here. super.method() looks the method up via the class's home object but keeps the current this. A derived constructor cannot touch this before super() (ReferenceError, the object doesn't exist yet). A constructor that explicitly returns an object discards this entirely. And bound functions are exotic objects: new on one ignores the bound this but keeps the bound arguments, which is why new (f.bind(x))() still constructs properly.

Binding as an API-design concern

At this band, treat binding as a contract with your consumers. If they can write const { fetch } = client and get a broken function, the bug lives in your API design. The options, with costs:

  • Arrow class fields. Auto-bound and survive destructuring, but you pay one closure per instance per method, they are invisible to super. in subclasses, and spying or mocking them on the prototype is awkward.
  • Constructor bind. Same safety, and methods stay on the prototype (mockable, inheritable); the price is a line of ceremony per method.
  • Closure-based factories. Skip this entirely. No binding bugs are possible, at the memory cost of per-instance functions.

The ecosystem played this arc out in public: React class components required manual binding, and hooks removed this from the model entirely.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

unlocks

more JavaScript