this and Context
What is this?
Section titled “What is this?”this is a special keyword that refers to the execution context — the object that is considered the receiver of the current function call. Unlike most languages, this in JavaScript is not bound at definition time; it is determined at call time by how the function is invoked.
this in different call patterns
Section titled “this in different call patterns”// 1. Method call — this is the object before the dotconst user = { name: 'Alice', greet: function() { return 'Hi, I am ' + this.name; },};console.log(user.greet()); // 'Hi, I am Alice'
// 2. Plain function call — this is undefined (strict mode) or globalThisfunction whoAmI() { return typeof this; // 'undefined' in strict mode}
// 3. Constructor call — this is the newly created objectfunction Person(name) { this.name = name;}const p = new Person('Bob');console.log(p.name); // 'Bob'Losing this
Section titled “Losing this”The most common pitfall: extracting a method from an object loses the binding.
const timer = { label: 'tick', start: function() { // After 0 ms, fn is called as a plain function — this.label is undefined setTimeout(function() { console.log(this.label); // undefined — 'this' is not timer }, 0); },};call, apply, and bind
Section titled “call, apply, and bind”These three methods let you explicitly set this.
function introduce(greeting, punctuation) { return greeting + ', I am ' + this.name + punctuation;}
const obj = { name: 'Carol' };
// call — pass arguments individuallyintroduce.call(obj, 'Hello', '!'); // 'Hello, I am Carol!'
// apply — pass arguments as an arrayintroduce.apply(obj, ['Hey', '.']); // 'Hey, I am Carol.'
// bind — returns a NEW function with this permanently boundconst boundIntroduce = introduce.bind(obj);boundIntroduce('Hi', '?'); // 'Hi, I am Carol?'Arrow functions and this
Section titled “Arrow functions and this”Arrow functions do not have their own this. They inherit this from the lexical enclosing scope — the surrounding regular function or module scope at the point where they are written.
const timer2 = { label: 'tick', start: function() { // Arrow function inherits 'this' from start() — which is timer2 setTimeout(() => { console.log(this.label); // 'tick' — correctly bound }, 0); },};This makes arrow functions the natural choice for callbacks inside methods.
A plain <script> runs in sloppy mode by default, where a detached this call silently falls back to the global object instead of throwing — the demo below opts into strict mode explicitly so the detached call fails the way it would inside an ES module or a class, both of which are strict mode automatically.
Runnable demo
Section titled “Runnable demo”The full this table
Section titled “The full this table”Six call shapes cover essentially every this bug you will meet in real code:
| Call shape | What this is | Loses binding? |
|---|---|---|
obj.method() | obj — the object before the dot | No |
const fn = obj.method; fn() (detached) | undefined in strict mode (throws on access); the global object in sloppy mode | Yes |
| Arrow function nested inside a method | Whatever this was in the enclosing method — inherited, not its own | No |
Class field arrow (prop = () => { ... }) | The instance, permanently — bound once at construction | No |
Regular function passed to setTimeout/array callback | Not the object you expected — a timer object or undefined, never the object it was “on” | Yes |
fn.bind(obj) / .call(obj) / .apply(obj) | Explicitly obj, forced regardless of how the result is later called | No (that is the point) |
flowchart TD A["How is the function called?"] --> B["obj.method() -- this = obj"] A --> C["detached: const fn = obj.method; fn() -- this LOST"] A --> D["arrow nested in a method -- this inherited from enclosing scope"] A --> E["class field arrow -- this = instance, bound at construction"] A --> F["passed to setTimeout / array callback as a regular function -- this LOST"] A --> G["fn.bind(obj) / .call(obj) / .apply(obj) -- this forced to obj"]
The demo above already covers the method call and the detached call. This one covers the other three shapes that most often surprise a reviewer: an arrow nested in a method, a class field arrow, and a regular function handed to setTimeout.
Output (verified on Node 24)
detached class-field arrow still works: 1
nested arrow this.label: inherited-arrow
regular setTimeout callback this.label: undefined
arrow setTimeout callback this.label: tickNotice the ordering in the output: the two setTimeout callbacks always run after the synchronous code (including the class-field arrow log) has finished — this is the event loop deferring their macrotask callbacks, covered in depth in the Event Loop & Async module.
Mental Model
Section titled “Mental Model”Think of this as an invisible extra parameter that JavaScript fills in at the call site, not at the definition site — like an unlabeled first argument every non-arrow function secretly receives. obj.method() fills it with obj. A bare fn() fills it with undefined (strict) or the global object (sloppy). fn.call(obj)/fn.apply(obj)/fn.bind(obj) let you fill it in yourself. Arrow functions are the one exception: they never receive this invisible parameter at all — an arrow function textually reads whatever this was already in scope where it was written, exactly like any other variable it closes over.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Regular function as a method | Gets its own dynamic this, reusable across different receiver objects (fn.call(otherObj)) | Easy to lose this by extracting or passing the function as a callback |
| Arrow function as a callback | Cannot lose this — it was never its own to lose | Cannot be re-targeted with call/apply/bind; unsuitable as an object method itself |
| Class field arrow | Auto-bound once, safe to pass as an event handler or callback directly | One function instance is created per object instance, not shared on the prototype (a small memory cost for many instances) |
.bind(obj) | Makes the correct this permanent and impossible to override, even accidentally | Creates a new function object on every call — avoid calling .bind() inside a hot loop |
Common Misconceptions
Section titled “Common Misconceptions”- “Arrow functions are just shorter syntax for regular functions.” They also change semantics: no own
this, no ownarguments, and they cannot be used as constructors (new arrowFn()throws). - “If a function is defined inside an object literal,
thisalways refers to that object.” Only true for a function written withmethod() {}ormethod: function() {}shorthand and called asobj.method(). The same function detached and called alone loses that binding entirely. - “
bindruns the function immediately.”fn.bind(obj)returns a brand-new function without callingfn; you must still invoke the result. - “An arrow class field costs nothing compared to a prototype method.” It is bound once per instance, which is exactly what you want for a callback — but it does mean N instances create N separate function objects, unlike a prototype method which is shared by every instance.
Reading AI-generated code
Section titled “Reading AI-generated code”The most common this bug in AI-generated code is passing an object method directly as a callback — setTimeout(obj.method, 1000), array.map(obj.method), emitter.on('event', obj.method) — all of which detach the method from obj at the moment they are passed, exactly like the classic detached-call bug, just one level of indirection away from being obvious.
🐛 Spot the bug
class Poller {constructor(url) { this.url = url; this.attempts = 0;}
poll() { this.attempts += 1; console.log('polling', this.url, 'attempt', this.attempts);}
start() { setInterval(this.poll, 1000); // looks fine — poll is "the method"}}
new Poller('https://example.com').start();Show the answer
setInterval(this.poll, 1000) passes the poll function by itself, detached from the Poller instance — exactly the same bug as const fn = obj.method; fn(). When the timer fires, poll runs with this as undefined (class bodies are strict), so this.attempts += 1 throws TypeError: Cannot read properties of undefined. The fix is to preserve the binding explicitly: setInterval(this.poll.bind(this), 1000), setInterval(() => this.poll(), 1000), or declare poll as a class field arrow (poll = () => { ... }) so it is always bound to the instance, however it is passed around.