Arrow Functions and `this` — From First Principles to Practice
Nearly every JS/TS developer has fallen into this trap. Memorizing rules like “arrow functions lock this” won’t stick. Instead of starting from conclusions, let’s begin at the root: what this actually is.
this Isn’t “Myself” — It’s “Who Called Me”
It’s easy to assume this points to the current object. But in JS, there’s really only one core rule:
The value of
thisdepends on what’s before the dot.at the moment the function is called.
const obj = {
name: "Alice",
sayName() {
console.log(this.name);
},
};
obj.sayName(); // "Alice" — dot is before obj, so this = obj
Same function, different invocation — this changes:
const fn = obj.sayName;
fn(); // undefined (strict mode) or window (non-strict)
// ↑ nothing before the dot — this is lost
So: this is not determined by where the function is defined, but by how it’s called. This is the root cause of all confusion around this.
The Four Faces of this
Whenever you run into a this problem, come back to this table:
| Invocation Pattern | Code | this Is? |
|---|---|---|
| Method call | obj.foo() | obj (whatever’s before the dot) |
| Bare function call | foo() | undefined (strict) / window (non-strict) |
new call | new Foo() | The newly created empty object |
| Hard binding | foo.call(x), foo.apply(x), foo.bind(x) | Whatever x you passed |
Let’s see it in code:
function showThis() {
console.log(this);
}
const obj = { name: "obj", showThis };
// Pattern 1: Method call
obj.showThis(); // → { name: "obj", showThis: ... } this = obj
// Pattern 2: Bare function call
const fn = obj.showThis;
fn(); // → undefined (strict mode)
// Pattern 3: new call — creates a new object {}, this points to it, then runs showThis as constructor
new showThis(); // → showThis {} (an empty object)
// Pattern 4: Hard binding
showThis.call("hello"); // → "hello"
showThis.apply(42); // → 42
const bound = showThis.bind("I am bound");
bound(); // → "I am bound"
For what exactly happens with
new, see my other article → new Call — How a Function Becomes an Object
For a quick primer on
call/apply/bind→ Remember call, apply, bind in Three Sentences
Same function showThis — this is different each time. So the phrase “this points to the current object” is wrong from the start.
In fact — this points to the caller, not the definer.
The Classic this Loss Pitfall in Class Methods
Back to classes:
class Counter {
count = 0;
increment() {
this.count++; // What is `this` here? Check the call site!
console.log(this.count);
}
}
const c = new Counter();
c.increment(); // Method call — instance `c` calls it, so this = c; instance count goes from 0 to 1
But if you pass the method to something else:
const fn = c.increment; // Extract the method
fn(); // this.count is NaN!
// Nothing before the dot on fn — this is undefined
Typical callback scenarios:
// Note: the method is being passed! It looks like c.increment, but when actually called, there's nothing before the dot!
<button onClick={c.increment}>+1</button>
// On click, this = undefined — this.count throws
// Same issue with setTimeout:
setTimeout(c.increment, 1000);
Arrow Functions Have No this of Their Own
This is where arrow functions reveal their purpose:
Arrow functions don’t have their own
this. When you accessthis, it looks outward and grabs whatever it finds.
But what exactly is “outward”? Class field initializers are essentially equivalent to being written inside the constructor:
class Counter {
count = 0;
increment = () => {
console.log(this);
};
}
// Equivalent to:
class Counter {
constructor() {
this.count = 0;
this.increment = () => {
console.log(this);
}; // i.e., the instance created by `new`
}
}
It captures the this from the constructor — the instance created by new.
So arrow functions aren’t created when the class is declared — they’re created each time new Counter() runs the constructor. At that moment, the outer this = the new instance, and the arrow function captures it.
A regular function asks “Who is calling me?” An arrow function asks “Where was I created?”
Compare:
class Demo {
name = "Demo";
// Regular method
normalMethod() {
console.log("normal:", this?.name);
}
// Arrow property
arrowMethod = () => {
console.log("arrow:", this?.name);
};
}
const d = new Demo();
// Scenario 1: Direct call — both work fine
d.normalMethod(); // "normal: Demo"
d.arrowMethod(); // "arrow: Demo"
// Scenario 2: Extract the methods
const normal = d.normalMethod;
const arrow = d.arrowMethod;
normal(); // "normal: undefined" — this is lost
arrow(); // "arrow: Demo" — this is locked in
// Scenario 3: As callbacks
setTimeout(d.normalMethod, 100); // — this is lost
setTimeout(d.arrowMethod, 100); // — works fine
So When Should You Use Arrow Functions?
A simple heuristic: Will your function be passed elsewhere to be called?
- No: A regular method is fine — more memory-efficient since the method is shared on the prototype.
- Yes: Use an arrow function —
thisis bound to the instance itself, at the cost of each instance owning its own copy.