Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
Published on HivePostify by @scipio · Fri Sep 04 2026
Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
What will I learn - You will learn that this is decided by HOW a function is called, not where it is defined; - the five binding rules that between them explain every value this can take; - why a method "loses" its this when 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 this bugs 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](https://hive.blog/hive-196387/@scipio/learn-js-series-1-what-is-javascript-why-it-runs-everywhere-and-how-to-run-it) - [Learn JS Series (#2) - Variables and Bindings](https://hive.blog/hive-196387/@scipio/learn-js-series-2-variables-and-bindings) - [Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/learn-js-series-6-numbers-ieee-754-why-01-02-is-not-03-and-how-to-cope) - [Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression](https://hive.blog/hive-196387/@scipio/learn-js-series-7-control-flow-ifelse-switch-and-the-ternary-expression) - [Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which](https://hive.blog/hive-196387/@scipio/learn-js-series-8-loops-for-while-forof-forin-and-when-to-use-which) - [Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/learn-js-series-13-truthiness-equality-and-coercion-vs-done-properly) - [Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator](https://hive.blog/hive-196387/@scipio/learn-js-series-14-mini-project-a-command-line-tip-calculator) - [Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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](https://hive.blog/hive-196387/@scipio/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)](https://hive.blog/hive-196387/@scipio/learn-js-series-19-callbacks-and-the-callback-pattern-before-we-reach-promises) - [Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures](https://hive.blog/hive-196387/@scipio/learn-js-series-20-default-rest-and-spread-flexible-function-signatures) - [Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way](https://hive.blog/hive-196387/@scipio/learn-js-series-21-destructuring-parameters-named-arguments-the-js-way) - [Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case](https://hive.blog/hive-196387/@scipio/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:
js 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:
js 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:
js 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:
js 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:
js 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:
js "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:
js 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:
js 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":
js 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:
js 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:
js 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:
js 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.
Tags: #programming