this & binding
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
- Method call.
obj.method():thisis the object before the dot. - Plain call.
fn():thisisundefinedin strict mode (the global object in old sloppy code). - Constructor.
new Fn():thisis the brand-new object being built. - Explicit.
fn.call(obj)andfn.apply(obj)run the function withthis = obj;fn.bind(obj)returns a new function permanently tied toobj.
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.
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()readsuserlate, 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 toremoveEventListenerlater.
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.
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
thisentirely. 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.