B12 · Part 1 · Beginner
The this keyword
Not where it was written — who called it.
08On this page
this is an extra parameter you never declared
Every ordinary function call quietly passes one more argument than you
wrote. You never name it, you never see it in the parameter list, and
inside the body it is called this.
What goes in it is not decided when you write the function. It is
decided at the moment of the call, by the shape of the call
itself. That single sentence dissolves most this
confusion, because it means the same function can be handed a
different this on every line that calls it.
function whoAmI() {
return this;
}
const a = { name: "a", whoAmI };
const b = { name: "b", whoAmI };
a.whoAmI(); // this is a
b.whoAmI(); // this is b — same function, different answer
whoAmI(); // this is undefined (strict) — nobody to be
Say it like this → "Reading a function
tells you what it does. Only the call site tells you what
this is."
One function, six call sites
The same describe function, called six different ways.
Step through and watch which rule fires and what this
lands on.
Which rule fires
this becomes
Result
Dry run: the same describe() function, six call sites
| Call | Rule that fires | this becomes | Result |
|---|---|---|---|
user.describe() | 3 — implicit binding | user | "ana" |
bare() (describe pulled off user first) | 4 — default binding | undefined | TypeError: cannot read name of undefined |
describe.call({ name: "called" }) | 2 — explicit binding | { name: "called" } | "called" |
describe.bind(user)() | 2 — explicit binding | user, permanently | "ana" |
new Tag() | 1 — new binding (wins) | the brand-new object | "new" |
setTimeout(user.describe, 0) | 4 — default binding | undefined | TypeError again |
Same function, five different outcomes — nothing about
describe's own code ever changes. The two default-binding
rows are the same bug wearing two different outfits: both strip the dot
off before the call happens, one by assignment, one by passing the
method as a callback.
The four rules, ranked
Four rules can set this, and when more than one could
apply, the higher-ranked one wins. Work down the list and stop at the
first match.
| Rank | Rule | The call looks like | this becomes |
|---|---|---|---|
| 1 — wins | new binding | new Fn() |
the brand-new object being built |
| 2 | Explicit binding | fn.call(obj), .apply(obj), .bind(obj) |
exactly the object you handed over |
| 3 | Implicit binding | obj.method() |
the object left of the dot |
| 4 — default | Default binding | a plain fn() |
undefined in strict mode and modules; the global object in old sloppy scripts |
Only the last dot counts
With implicit binding it is easy to assume the whole path matters. It does not — only the object immediately left of the final dot does.
const app = {
name: "app",
inner: {
name: "inner",
who() { return this.name; },
},
};
app.inner.who(); // "inner" — not "app"
call, apply and bind
function greet(greeting, mark) {
return greeting + ", " + this.name + mark;
}
const user = { name: "ana" };
greet.call(user, "Hi", "!"); // "Hi, ana!" — arguments listed
greet.apply(user, ["Hi", "!"]); // "Hi, ana!" — arguments in an array
const hi = greet.bind(user, "Hi");
hi("!"); // "Hi, ana!" — returns a new function instead of calling
The mnemonic writes itself: call takes commas,
apply takes an array — and bind is the odd
one out because it
does not run anything, it hands you a new function with
this permanently attached. Binding an already-bound
function a second time does nothing; the first weld holds.
Arrow functions opt out entirely
An arrow function has no this of its own. None of the
four rules apply to it. When you write this inside an
arrow, it is resolved like any other variable — by looking outwards
through the scopes it was written in, exactly the way the
scope chain works.
const timer = {
label: "timer",
withFunction() {
[1].forEach(function () {
console.log("function callback:", this === undefined ? "undefined" : this.label);
});
},
withArrow() {
[1].forEach(() => {
console.log("arrow callback: ", this.label);
});
},
};
timer.withFunction();
timer.withArrow();
The plain function callback gets its own this by rule 4 —
nobody called it with a dot, so it is undefined. The
arrow has none to get, so it keeps reading the
this belonging to withArrow, which is
timer. This is the entire reason arrows took over
callbacks.
⚠ Never use an arrow as an object method
const o = { name: "o", who: () => this.name }; looks
tidier and is broken. The arrow was written at the top level, not
inside any function, so its this is whatever the module
or script has — never o. Methods want
who() { ... }.
How this gets lost — and the three fixes
Almost every real this bug is the same move: the function
gets separated from its dot. Three ways it happens, all identical
underneath.
const counter = {
count: 0,
increment() { this.count++; return this.count; },
};
// 1. Pulled out into a variable
const inc = counter.increment;
inc(); // 💥 this is undefined
// 2. Passed as a callback
button.addEventListener("click", counter.increment); // 💥
setTimeout(counter.increment, 100); // 💥
// 3. Destructured out
const { increment } = counter;
increment(); // 💥 same thing, shorter spelling
The fixes, in the order you should reach for them:
// A. Wrap it in an arrow — the call keeps its dot
setTimeout(() => counter.increment(), 100);
// B. Bind it, when you need a reusable detached function
const inc = counter.increment.bind(counter);
// C. In a class, make it a field holding an arrow — bound at construction
class Counter {
count = 0;
increment = () => { this.count++; }; // safe to pass anywhere
}
Option A is usually right, because it keeps the object and the method together at the call site where a reader can see them. Option C costs one function per instance rather than one per class, which only matters when you are making thousands of them.
this in a class
class Timer {
constructor(label) {
this.label = label; // rule 1 — this is the new instance
this.ticks = 0;
}
tick() { // rule 3 when called as timer.tick()
this.ticks++;
}
}
const t = new Timer("mine");
t.tick(); // fine — there is a dot
[1, 2, 3].forEach(t.tick); // 💥 no dot. Classes are strict, so this is undefined
Class bodies are always in strict mode, which means a lost
this inside a class throws rather than silently
writing properties onto the global object. That is a feature: the old
sloppy-mode behaviour turned this same bug into a mysterious global
variable instead of an error.
this in an event handler
button.addEventListener("click", function () {
this; // the element the listener is attached to
this.classList.add("on");
});
button.addEventListener("click", () => {
this; // NOT the button — whatever the surrounding scope had
});
Rule In a handler, prefer
event.currentTarget over this. It means the
same element, it keeps working when you switch to an arrow, and it
says out loud what it is. Note currentTarget is the
element you attached to; event.target is whatever was
actually clicked, which may be a child.
The traps, collected
-
Passing a method as a callback —
onClick={obj.method},setTimeout(obj.method),arr.map(obj.method). The dot does not travel. -
An arrow as an object method — it reads
thisfrom the file, not the object. -
A plain
functioncallback inside a method — it gets default binding. Use an arrow, or the second argument offorEach/map, which setsthisfor you. - Assuming the whole path binds — only the last dot counts.
-
Expecting a re-
bindto work — the first one is permanent.
Say it like this → "
this is
set by the call, not the definition. Check for new, then
for call/apply/bind, then for a
dot, and if none of those apply it is undefined. Arrows
have no this at all — they borrow the one around them."
Interview answer, one sentence
"this is not where a function was written but how it was called: new, explicit bind/call/apply, a method call and a plain call decide it in that order of precedence, while arrow functions have no this of their own and inherit it."
Before you move on
- Rank the four binding rules from memory and say which one wins when more than one could apply.
- Explain why
const fn = obj.method; fn();losesthis, and name the fix you'd reach for first. - Say why an arrow function should never be used as an object method, in terms of where it looks up
this. - Trace
thisthrough.call,.apply, and.bindon the same function, and say what makes.binddifferent from the other two. - Explain why
event.currentTargetis a safer choice thanthisinside an event handler.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series