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 sloppyOne 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
| Priority | Rule | Call looks like | this becomes |
|---|---|---|---|
| 1 (highest) | new binding | new Fn() | the brand new object |
| 2 | Explicit binding | fn.call(o), fn.apply(o), fn.bind(o) | o |
| 3 | Implicit binding | o.fn() | o (the thing left of the dot) |
| 4 (lowest) | Default binding | fn() | 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.
- Rule 4: default binding A plain call with nothing in front of it. In a module or any strict code,
thisisundefined. In a sloppy-mode script it is coerced toglobalThis, which is the source of a thousand accidental globals. - Rule 3: implicit binding Only the last dot counts. In
a.b.c(),thisinsidecisb, nota. - Rule 2: explicit binding
callandapplysetthisfor one call.bindreturns a new function withthiswelded on permanently. - Rule 1: new binding
newcreates a fresh object and passes it asthis. It beats everything else, including a hardbind.
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); // nullES 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.
| Fix | Code | Notes |
|---|---|---|
| Wrap in an arrow | setTimeout(() => counter.increment()) | The dot survives inside the arrow. Usually the clearest fix. |
| Bind at the boundary | setTimeout(counter.increment.bind(counter)) | Explicit, but creates a new function each time you call it. |
| Bind in the constructor | this.handle = this.handle.bind(this) | One bound copy per instance. The pre-arrow React idiom. |
| Use a class field arrow | handle = () => { ... } | Per-instance, auto-bound, but not on the prototype. |
| Close over the object | const 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 callundefined(strict) orglobalThis(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 levelundefined
A function does not own its
this. The call site lends it one.
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 too, so it also returns"own".readis never called, so nothing throws:typeof readis just"function". Callingread()in strict code would be the TypeError. - Which call site wins when several rules could apply?
newbeats explicit, which beats implicit, which beats default — The precedence is fixed:new> explicit (call/apply/bind) > implicit (obj.fn()) > default (fn()). That is whynew (Fn.bind(other))()still gets the fresh object rather thanother.- Why can
callnot changethisinside an arrow function? - Arrows have no
thisbinding of their own, sothisis resolved lexically like any variable — An arrow function never creates athisbinding. Thethisyou write inside it belongs to the enclosing scope, socall,applyandbindhave nothing to overwrite. They still pass arguments, they just cannot changethis. - In a plain
fn()call inside an ES module, what isthis? undefined— Modules are always strict, so default binding leavesthisasundefinedinstead of coercing it to the global object. This is whythis.x = 1in a stray helper throws a TypeError in a module but silently creates a global in an old script.
Common mistakes
- Assuming
thisfollows the same lexical rules as variables. It does not, except in arrows. - Writing an arrow function as an object method, then wondering why
thisis undefined. - Passing
obj.methodas a callback and losing the object. - Thinking
thisina.b.c()isa. Only the last dot counts. - Testing
thisbehaviour in a sloppy-mode script and being surprised in a module, whereundefinedis not coerced.
Takeaways
thisis bound at call time, not at definition time.- Four rules, in order:
new, explicit, implicit, default. - Default binding is
undefinedin strict code andglobalThisin 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.