Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
What will I learn
- You will learn that
thisis decided by HOW a function is called, not where it is defined; - the five binding rules that between them explain every value
thiscan take; - why a method "loses" its
thiswhen you detach it, and how to spot it instantly; - how arrow functions opt out of these rules by borrowing the surrounding
this; - practical fixes for the most common
thisbugs you will actually hit.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-21 read, especially objects (ep12) and arrow functions (ep16).
Difficulty
- Beginner
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case (this post)
Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
Solutions to Episode 21 Exercises
Exercise 1 - destructuring and swapping:
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
console.log(red, green, blue); // 255 128 0
let a = 1, b = 2;
[a, b] = [b, a]; // swap with no temp variable
console.log(a, b); // 2 1
The insight: array destructuring reads the entire right side first, then assigns, so [a, b] = [b, a] swaps cleanly without any scratch variable.
Exercise 2 - a box with a defaulted char:
function drawBox({ width, height, char = "*" }) {
return `${width}x${height} box of '${char}'`;
}
console.log(drawBox({ width: 4, height: 2, char: "#" })); // "4x2 box of '#'"
console.log(drawBox({ width: 4, height: 2 })); // "4x2 box of '*'"
The insight: the per-property default char = "*" fills in only when the caller omits that specific key.
Exercise 3 - separating name from the rest:
function describe({ name, ...details }) {
return `${name} has extras: ${JSON.stringify(details)}`;
}
console.log(describe({ name: "scipio", level: 7, city: "amsterdam" }));
// "scipio has extras: {"level":7,"city":"amsterdam"}"
The insight: rest in destructuring gathers the leftover properties into a fresh object; adding a = {} default would let describe() with no argument work in stead of throwing on undefined.
Right, that clears the decks. Now for the notorious this. It has a reputation for being one of the hardest corners of JavaScript, but the reputation is undeserved -- people struggle with it because they look for the answer in completely the wrong place. Fix where you look, and this becomes almost mechanical.
The core principle: this is about the call site
Here is the single idea that unlocks everything, so read it twice: in a regular function, this is NOT decided by where the function is written. It is decided by how the function is called -- what we call the "call site". The very same function can have a different this on every single call, depending purely on the way you invoke it.
That is a genuinely different mental model from most languages you may know. In a class-based language, this (or self) is nailed to the object the method belongs to. In JavaScript a plain function is a free-floating thing, and this is a hidden extra argument that gets filled in at the moment of the call, based on how you wrote that call. So stop asking "what is this here?" while staring at the function body. Start asking "how is this function being called?". Five rules cover every case there is, and I will walk you through all five.
Rule 1: method call - this is the object before the dot
When you call a function as a method, obj.method(), this is the object to the left of the dot. This is the most common case and the most intuitive one:
const user = {
name: "scipio",
greet() {
return `Hi, I am ${this.name}`; // 'this' is 'user'
},
};
console.log(user.greet()); // "Hi, I am scipio"
this is user because we called user.greet() -- user is the thing sitting before the dot. This is exactly what makes this useful in the first place: it lets ONE function serve MANY objects. Watch the same method work for a different object without any change to the code:
const admin = { name: "root", greet: user.greet };
console.log(admin.greet()); // "Hi, I am root" - same function, different 'this'
Same greet function, but calling it through admin makes this be admin. That reuse is the whole point of this. The confusion only starts when the same function is called in a way that has no object before the dot, which is our next rule.
Rule 2: plain function call - this is undefined (strict) or global
When you call a function directly, with nothing before it, this is undefined in strict mode (which ES modules and classes use automatically), or the global object in old "sloppy" mode. Modern JavaScript is effectively strict everywhere, so train yourself to read a bare fn() call as giving this === undefined:
"use strict";
function standalone() {
return this; // no object before it -> undefined in strict mode
}
console.log(standalone()); // undefined
This rule is the source of the single most classic this bug in the language: the "lost this". It bites the moment you detach a method from its object and call it on its own:
const counter = {
count: 0,
increment() { this.count += 1; return this.count; },
};
const inc = counter.increment; // detached! no more 'counter.' before it
// console.log(inc()); // ERROR (runtime): Cannot read properties of undefined (this is undefined)
console.log(counter.increment()); // 1 - fine, called as a method
Look closely, because this is subtle and important: inc() is a plain call, so rule 2 applies and this is undefined, and then this.count blows up. The function itself did not change one bit. The way we called it did. Rule 1 versus rule 2 is decided entirely at the call site, counter.increment() versus inc(), not in the definition.
This is precisely the trap you fall into when you pass a method somewhere as a callback. Every one of these detaches increment the same way const inc = ... did:
const events = { name: "clicks", log() { return `event: ${this.name}`; } };
// each of these strips the method off its object -> 'this' becomes undefined
// setTimeout(events.log, 100); // called later as a plain function
// [1, 2].forEach(events.log); // called by forEach as a plain function
// element.addEventListener("click", events.log); // browser calls it plainly
console.log(events.log()); // "event: clicks" - only works called AS a method
Whenever you hand obj.method to something that will call it for you later, you have detached it. Remember that, and half of all this bugs simply stop happening to you.
Rule 3: explicit binding - call, apply, and bind
You do not have to leave this to chance. You can force it to be whatever you want using call, apply, or bind (which get their own full episode next, so this is a taste). All three say the same thing: "run this function, but with this set to the object I hand you":
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const person = { name: "scipio" };
console.log(greet.call(person, "Hi")); // "Hi, scipio" - this = person
console.log(greet.apply(person, ["Yo"])); // "Yo, scipio" - args passed as an array
const bound = greet.bind(person); // returns a NEW function locked to person
console.log(bound("Hey")); // "Hey, scipio"
call and apply invoke the function immediately, differing only in how you pass the arguments (call takes them one by one, apply takes them as a single array). bind is the odd one out: it does not call anything, it returns a brand new function permanently locked to that this. And that is exactly how you cure the "lost this" from rule 2 -- you pre-bind the method before you detach it:
const box = {
count: 0,
tick() { this.count += 1; return this.count; },
};
const tick = box.tick.bind(box); // locked to box forever
console.log(tick()); // 1
console.log(tick()); // 2 - works even though it is now a bare call
Because tick is bound, it no longer cares that there is no box. in front of it -- the this is baked in and cannot be overridden by the call site. That is the property that makes bind the go-to fix for callbacks.
Rule 4: new - this is the freshly created object
When you call a function with new (making it a constructor, a topic we unpack properly in Phase 3), JavaScript hands this a brand new empty object for the function to set up:
function User(name) {
this.name = name; // 'this' is the new object being built
this.active = true;
}
const u = new User("scipio");
console.log(u); // User { name: 'scipio', active: true }
Mechanically, new User(...) does four things in order: it creates a fresh empty object, points this at it, runs the function body so it can fill the object in, and (unless you explicitly return another object) hands that new object back to you. We will pull new, prototypes, and constructors fully apart later; for now just log it as the fourth distinct way this gets its value.
Rule 5: arrow functions - no this of their own
The fifth rule is the exception we first met in episode 16, and it is the one that makes modern JavaScript pleasant: arrow functions ignore all four rules above. An arrow has no this of its own at all. Instead it uses the this of the enclosing scope where it was written -- what we call lexical this. Because it is fixed by location, not by the call, no call site can change it. This is what makes arrows perfect for callbacks nested inside a method:
const timer = {
seconds: 0,
start() {
// the arrow keeps start()'s 'this' (the timer), even though
// setInterval will call it later as a plain function
setInterval(() => {
this.seconds += 1; // 'this' is STILL 'timer', correct!
}, 1000);
},
};
// timer.start(); // ticks correctly because the arrow inherited 'this'
Had that callback been a regular function, rule 2 would kick in -- setInterval calls it as a plain function, so this would be undefined, and this.seconds would throw. The arrow sidesteps the whole mess by not having its own this to lose. This one property is the reason arrows took over as the default shape for callbacks in the last decade.
The same trap shows up with array methods, and it is worth seeing side by side. Here a regular-function callback loses this, while an arrow keeps it:
const account = {
owner: "scipio",
amounts: [10, -5, 20],
reportBroken() {
this.amounts.forEach(function (amount) {
// rule 2: this callback is called plainly by forEach -> 'this' is undefined
// console.log(`${this.owner}: ${amount}`); // would throw
return amount;
});
},
reportFixed() {
this.amounts.forEach((amount) => {
// rule 5: arrow borrows reportFixed()'s 'this' -> still 'account'
console.log(`${this.owner}: ${amount}`);
});
},
};
account.reportFixed(); // scipio: 10 / scipio: -5 / scipio: 20
Same object, same loop, two different callbacks -- and the only thing that changed is function versus =>. That is the practical heart of this whole episode.
Putting the rules in order
When you meet a this in the wild and wonder what it is, do not guess -- check the call site against the rules, roughly in this priority order. Is it an arrow function (rule 5, so use the enclosing this)? Was it called with new (rule 4)? With call, apply, or bind (rule 3)? As obj.method() (rule 1, the object before the dot)? Or as a plain fn() (rule 2, undefined)? Exactly one of those always applies:
const obj = {
value: 42,
regular() { return this?.value; }, // rule 1 when called as obj.regular()
arrow: () => this?.value, // rule 5: 'this' is the OUTER scope, not obj
};
console.log(obj.regular()); // 42 - method call, 'this' is obj
console.log(obj.arrow()); // undefined - arrow used the module's 'this', not obj
This is the flip side of rule 5, and it catches beginners constantly: an arrow is the WRONG choice for a top-level method (as we warned back in episode 16). obj.arrow() does NOT give the arrow obj as its this -- the arrow already fixed its this to wherever it was defined (module scope here, where this is undefined). So the working rule of thumb is short and mechanical: a method that needs this must be a regular function; a callback that wants the surrounding this should be an arrow. Get those two the right way round and the notorious confusion mostly evaporates.
Why does JavaScript even work this way?
It is fair to ask why the language chose something this slippery. The honest answer is that dynamic this is what lets a single function be shared across many objects and even "borrowed" onto objects it was never written for (that is what call and apply are for). The cost of that flexibility is that this is not fixed at definition time. Arrow functions were added later precisely because, most of the time, in callbacks you actually WANT the boring, predictable, lexical behaviour -- so ES2015 gave you a function form that opts out of the dynamic rules entirely. Knowing both halves, dynamic for methods and lexical for callbacks, is knowing this.
How other languages handle this
A lot of you arrived here from the Learn Python Series, with a few from Rust and Go, so a quick look sideways helps place JavaScript's choice in context. The short version: most languages make the receiver explicit or fixed, which is exactly why they never suffer the "lost this" bug.
Python does not hide the receiver at all -- it is the first parameter, spelled self, right there in the signature. And crucially, a Python method stays bound to its instance even when you pull it off and pass it around, so the JavaScript detachment bug simply cannot happen:
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
c = Counter()
inc = c.increment # a "bound method" - still remembers c
print(inc()) # 1
print(inc()) # 2 - no lost-self bug, unlike JavaScript
Where JavaScript decides this at the call site, Python binds self when you access the method through an instance, and keeps it bound. That is the whole difference in one example.
Rust goes further and makes it a compile-time affair. There is no free-floating receiver at all -- self is an explicit parameter and the method is resolved against a concrete type, so nothing analogous to this can wander:
struct Counter { count: u32 }
impl Counter {
fn increment(&mut self) -> u32 {
self.count += 1;
self.count
}
}
fn main() {
let mut c = Counter { count: 0 };
println!("{}", c.increment()); // 1
}
That &mut self is spelled out, checked by the compiler, and cannot be undefined at runtime -- the exact opposite of JavaScript's "figure it out when you call me".
Go, true to its minimalist form, uses an explicit receiver named right before the method, and a method value keeps its receiver when you store it in a variable -- again, no surprise rebinding:
package main
import "fmt"
type Counter struct{ count int }
func (c *Counter) Increment() int {
c.count++
return c.count
}
func main() {
c := &Counter{}
inc := c.Increment // method value - remembers c
fmt.Println(inc()) // 1
fmt.Println(inc()) // 2
}
Four languages, one clear lesson: JavaScript is the outlier that lets this be decided by the call rather than fixed at definition. That flexibility is genuinely powerful (borrowing methods, one function across many objects), but it is also why you, the JavaScript programmer, have to learn five rules where a Python or Rust programmer learns roughly one. Having said that, once the five rules are in your head, they really are all there is -- there is no sixth case waiting to ambush you. ;-)
Try it yourself
Three exercises, increasing in difficulty. Type them out, run them, and predict the output before you check -- that gap between your guess and reality is where the learning actually lands. Full solutions open the next episode.
- Create an object
dogwith anameand aspeak()method that returns"name says woof"usingthis.name. Call it as a method (works), then assign the method to a bare variable and call that (observe the failure), and explain in one line which rule applies to each call. - Write a standalone function
introduce()that usesthis.name, plus an object{ name: "scipio" }. Usecalland thenbindto makeintroducework with that object, and explain the difference betweencallandbindin a single sentence. - Build an object with a method that uses
setTimeoutinternally and needsthis. First write the callback as a regular function and watchthisbreak, then switch it to an arrow and watch it work. Explain, using rule 2 and rule 5, exactly why the arrow fixed it.
So what did we actually cover?
thisis determined by how a function is CALLED (the call site), not where it is defined -- that one shift in where you look explains everything else.- Rule 1, method call
obj.fn():thisis the object before the dot, which is what lets one function serve many objects. - Rule 2, plain call
fn():thisisundefined(strict mode) -- the source of the "lost this" bug whenever a method is detached or passed as a callback. - Rule 3,
call/apply/bind: you setthisexplicitly;bindreturns a permanently locked function and is the standard fix for the lost-this problem. - Rule 4,
new fn():thisis the freshly created object (constructors, coming in Phase 3). - Rule 5, arrow functions: no own
this, they borrow the enclosing (lexical)this, which is why they are ideal for callbacks inside methods and wrong for top-level methods.
Next episode we zoom right into rule 3 and give it the full treatment: call, apply, and bind -- the three tools for controlling this explicitly, with the practical patterns (method borrowing, partial application, function currying) that make them genuinely worth reaching for.