Every beginner hits the same baffling bug. You write an object with a method that reads this.count, call it once and it works, then hand that method to a callback or store it in a variable — and suddenly this is wrong, this.count is undefined, and your total collapses to NaN. Nothing crashed, the method is unchanged, and yet it broke.
The reason is the most misunderstood rule in JavaScript: this is not a property of the function. It is decided at the call site — by how the function is invoked — not by where it is written. Move the exact same function from one line to another and this changes underneath you. This lesson makes that rule predictable instead of magical: you will read a call site and know this before the code runs.
flowchart TD M["obj.method()"] --> T1["this = obj"] P["plain fn()"] --> T2["this = global/undefined"] A["arrow fn"] --> T3["this = outer this"] style M fill:#e0900b,color:#fff style P fill:#e0900b,color:#fff style A fill:#e0900b,color:#fff
Two call shapes, two values of this
What sits to the left of the dot is everything. When you write obj.greet(), JavaScript does two things at once: it fetches greet, and it sets this to obj for the duration of the call. The object the method came off becomes this.
Strip the object away and the rule collapses. A plain call — greet() with nothing before it — has no object to assign, so this falls back to the global object (globalThis), or to undefined under strict mode. That fallback is the whole reason the opening bug exists: peel a method off its object and the object stops being this.
const user = {
name: "Ada",
greet() { return `Hi, I'm ${this.name}`; }
};
console.log(user.greet()); // "Hi, I'm Ada" — this = user
const detached = user.greet; // copy the function WITHOUT its object
console.log(typeof detached); // "function" (the object is already gone)
// detached() would be a plain call: this = undefined, and this.name throws.
Train your eye on the call site
The practical skill is purely visual: scan left from the call. team.getName() has team before the dot, so this is team. getName() on its own has nothing there, so this is the fallback. render.call(card) overrides the scan — call hands the object over explicitly. Read calls this way and the surprises stop, because nothing about the function's definition decides this; only the moment of invocation does.
One wrinkle is worth naming. In sloppy mode a lost this becomes the global object, so this.count = 1 silently creates a global and your program limps on with no error. In strict mode the same line sets this to undefined and fails loudly. Prefer the loud failure: it turns silent data corruption into a readable stack trace.
The classic trap: a method that loses its object
Here is that opening bug, made concrete. const solo = obj.bump copies the function without its object. Calling solo() is now a plain call, so this is undefined, and a line like this.count += 1 reads undefined.count and throws — or, in sloppy mode, silently writes a stray global and your real counter never moves.
This trips people constantly because the method looks fine and works the first time. It only breaks the moment you detach it: pass it to setTimeout, assign it to an event property, or return it from another function. Anywhere the caller invokes it as a bare fn() instead of obj.fn().
flowchart TD M["obj.bump()"] --> R1["this = obj ✓ count rises"] P["const f = obj.bump<br/>f()"] --> R2["this = undefined ✗ throws"] style R1 fill:#1e7f3d,color:#fff style R2 fill:#b91c1c,color:#fff
Two reliable fixes
The arrow function fix is the most common. Define the body as an arrow and it captures this from where it was written — the enclosing method — instead of consulting the call site. Arrows ignore the call site entirely, which is exactly what you want inside a callback.
The bind fix is the explicit one. obj.bump.bind(obj) returns a brand-new function whose this is permanently locked to obj, no matter how it is later called. Reach for arrows when you control the definition; reach for bind when you must hand someone else's method into a callback.
const team = {
lead: "Ada",
members: ["Grace", "Linus"],
// arrow inside map keeps this = team
roster() { return this.members.map(m => `${m} led by ${this.lead}`); }
};
console.log(team.roster());
const counter = { count: 0, bump() { this.count += 1; return this.count; } };
const fixed = counter.bump.bind(counter); // this locked to counter
console.log(fixed()); // 1
console.log(fixed.call({ count: 99 })); // 2 — bind wins, call can't override
flowchart LR C["fn.call(obj, a, b)"] --> N1["runs immediately"] A["fn.apply(obj, [a,b])"] --> N1 B["fn.bind(obj)"] --> N2["returns a new fn<br/>run it later"] style N1 fill:#3776ab,color:#fff style N2 fill:#b45309,color:#fff
What does this print? The method is called on the object, so this refers to that object.
const team = {
name: "A-Team",
getName() {
return this.name;
}
};
console.log(team.getName());
team.getName() calls getName with this set to team.
this.name is therefore "A-Team".
Write createBoundGreet(obj) that returns a new function. When called, the returned function should call obj.greet() and return its result. Use bind to make sure this stays correct even if the returned function is stored and called later.
function createBoundGreet(obj) {
// return a bound function
}
Use obj.greet.bind(obj) to create a function that always has this = obj.
Return that bound function.
function createBoundGreet(obj) {
return obj.greet.bind(obj);
}
getHandler is meant to return a function the UI can call later to bump this button's count. But it returns this.click unbound, so when the UI invokes the handler, this is wrong and the count never rises (or it throws). Repair getHandler so the returned function always bumps the right counter — use bind, or return an arrow.
function makeButton() {
return {
count: 0,
click() { this.count += 1; return this.count; },
getHandler() {
return this.click; // BUG: caller invokes it without the button
}
};
}
Returning this.click alone detaches it —
thisis lost when the caller runs it.Either return this.click.bind(this), or return () => this.click() so the arrow keeps this.
function makeButton() {
return {
count: 0,
click() { this.count += 1; return this.count; },
getHandler() {
return this.click.bind(this);
}
};
}
Write invoke(fn, thisArg, argsArray) that calls fn with this set to thisArg and the arguments taken from argsArray. Use apply so a single array spreads into however many parameters the function expects. This is the transfer task: nothing in the lesson hands you an array of arguments, so you must pick the right tool yourself.
function invoke(fn, thisArg, argsArray) {
// call fn with this = thisArg and arguments spread from argsArray
}
call takes arguments one by one; apply takes them as one array.
fn.apply(thisArg, argsArray) spreads the array across the parameters.
function invoke(fn, thisArg, argsArray) {
return fn.apply(thisArg, argsArray);
}
What does this print? The arrow inside map does not make its own this, so it borrows this from tag() — which is box.
const box = {
label: "tools",
items: ["a", "b"],
tag() {
return this.items.map(i => `${i}:${this.label}`);
}
};
console.log(box.tag());
tag() is called as box.tag(), so its this is box.
The arrow callback has no this of its own, so this.label stays box.label.
Recap
thisis set by the call site, not the definition.obj.m()⇒thisisobj; a barem()⇒thisisglobalThis(orundefinedin strict mode).- Peeling a method off its object —
const f = obj.m; f()— is the classic way to losethis. - Arrow functions inherit
thisfrom their enclosing scope and ignore the call site, which makes them ideal for callbacks. bindreturns a new function withthispermanently fixed;callandapplyrun immediately (comma-separated args versus one array).- Arrow functions cannot be rebound — their
thiswas sealed when they were written.
Next you'll see how functions can themselves accept other functions as arguments, which is the gateway to map, filter, and the rest of the array toolkit.