JavaScript this Keyword Explained: 4 Binding Rules & Arrow Functions
Master JavaScript's this keyword with the 4 binding rules in resolution order, arrow function behavior, and fixes for common callback and setTimeout bugs.
JavaScript this Keyword Explained
The value of this in JavaScript is not determined by where a function is defined, but by how it is called. Every regular function invocation resolves this by walking through four binding rules in a fixed priority order. Arrow functions break the rules entirely by inheriting this from their enclosing lexical scope.
The 4 Binding Rules (in Resolution Order)
1. new Binding (Highest Priority)
When a function is invoked with the new keyword, JavaScript creates a fresh object and binds this to it.
function User(name) {
this.name = name; // this = the new object
}
const u = new User("Ada");
console.log(u.name); // "Ada"
This rule beats every other rule, even bind().
2. Explicit Binding
call(), apply(), and bind() force this to a specific value.
function greet() { return `Hi, ${this.name}`; }
const person = { name: "Ren" };
greet.call(person); // "Hi, Ren"
greet.apply(person); // "Hi, Ren"
const bound = greet.bind(person);
bound(); // "Hi, Ren"
call and apply invoke immediately; bind returns a new function with this permanently locked. A bound function cannot be re-bound.
3. Implicit Binding
When a function is called as a method of an object, this is the object to the left of the dot.
const obj = {
name: "Kai",
greet() { return this.name; }
};
obj.greet(); // "Kai" — this = obj
Only the final reference matters. In a.b.c.method(), this is c, not a.
4. Default Binding (Lowest Priority)
If none of the above apply, this falls back to a default:
- Strict mode:
thisisundefined. - Sloppy mode:
thisis the global object (windowin browsers,globalin Node).
"use strict";
function show() { console.log(this); }
show(); // undefined
ES modules and class bodies are strict by default.
Arrow Functions: No Own this
Arrow functions do not have their own this. They capture this lexically from the surrounding scope at the moment of definition. The four rules above do not apply: call, apply, bind, and even new cannot change an arrow's this.
const obj = {
name: "Zoe",
regular() { return this.name; }, // this = obj
arrow: () => this.name // this = enclosing scope (not obj!)
};
obj.regular(); // "Zoe"
obj.arrow(); // undefined (or global.name)
Arrows also cannot be used with new — they throw TypeError.
Common Bugs and Fixes
Losing this in Callbacks
class Counter {
constructor() { this.count = 0; }
increment() { this.count++; }
}
const c = new Counter();
[1, 2, 3].forEach(c.increment); // TypeError: this is undefined
The method is extracted from the object and called plainly, so implicit binding is lost. Three fixes:
// Fix A: bind
[1,2,3].forEach(c.increment.bind(c));
// Fix B: arrow wrapper (captures outer this)
[1,2,3].forEach(() => c.increment());
// Fix C: pass thisArg (forEach supports it)
[1,2,3].forEach(c.increment, c);
setTimeout Losing Context
class Timer {
constructor() { this.seconds = 0; }
start() {
setTimeout(function () {
this.seconds++; // this = undefined / window
}, 1000);
}
}
Fix by using an arrow, which inherits this from start():
start() {
setTimeout(() => { this.seconds++; }, 1000);
}
Event Handlers
DOM event handlers set this to the element that fired the event — unless you attach an arrow, which keeps the surrounding this.
button.addEventListener("click", function () {
console.log(this); // the button element
});
button.addEventListener("click", () => {
console.log(this); // enclosing scope's this
});
Method Extraction
const q = document.querySelector; // extracted
q("div"); // TypeError: Illegal invocation
// DOM methods enforce a specific this-binding, and console.log
// happens NOT to require one — but querySelector, addEventListener,
// and many Element methods do. Fix with .bind or an arrow wrapper:
const q2 = document.querySelector.bind(document);
const q3 = (s) => document.querySelector(s);
Re-bind: const log = console.log.bind(console);
Class Methods and this
Class bodies are strict mode. Prototype methods rely on implicit binding, so extracting them loses this.
class Button {
constructor(label) { this.label = label; }
// Prototype method — this depends on call site
click() { console.log(this.label); }
// Public class field arrow — this is bound to instance forever
clickBound = () => { console.log(this.label); };
}
const b = new Button("Save");
const f1 = b.click; f1(); // undefined / crash
const f2 = b.clickBound; f2(); // "Save"
Class-field arrows are the modern replacement for constructor-based this.method = this.method.bind(this) patterns (common in React class components).
Quick Resolution Cheat Sheet
- Called with
new? → new object. - Called with
call/apply/ bound? → the specified object. - Called as
obj.fn()? →obj. - Otherwise? →
undefined(strict) or global (sloppy). - Arrow function? → ignore 1–4; use enclosing lexical
this.
When debugging, ask "how is this function being called right now?" — never "where was it written?". That single mental shift resolves the vast majority of this bugs.