The this keyword is one of the most misunderstood aspects of JavaScript — and one of the most important. Unlike many languages where this always refers to the current instance, in JavaScript this is determined at call time, not definition time. Its value depends entirely on how a function is invoked. Understanding the four binding rules and when arrow functions bypass them is essential for working with object methods, event handlers, callbacks, and classes without encountering the mysterious “this is undefined” error.
The Four this Binding Rules (Priority Order)
| Priority | How Called | this Value |
|---|---|---|
| 1 — new binding | new MyFn() |
Freshly created object |
| 2 — Explicit binding | fn.call(obj) / fn.apply(obj) / fn.bind(obj)() |
obj — whatever you pass |
| 3 — Implicit binding | obj.method() |
obj — the object before the dot |
| 4 — Default binding | fn() — plain call |
Strict mode: undefined / Sloppy: window |
| Exception — Arrow | Any call style | Lexical — inherits from enclosing scope, never changes |
call, apply, bind Compared
| Method | Syntax | Calls Immediately? | Args Format |
|---|---|---|---|
call |
fn.call(thisArg, a, b) |
Yes | Individual args |
apply |
fn.apply(thisArg, [a, b]) |
Yes | Array of args |
bind |
const f = fn.bind(thisArg, a) |
No — returns new function | Individual (pre-filled) |
Common this Loss Scenarios and Fixes
| Scenario | Problem | Fix |
|---|---|---|
| Method as callback | btn.addEventListener('click', obj.method) |
Arrow wrapper or .bind(obj) |
| setTimeout / setInterval | setTimeout(this.update, 1000) |
setTimeout(() => this.update(), 1000) |
| Destructured method | const { method } = obj; method(); |
const method = obj.method.bind(obj) |
| Array forEach/map | items.forEach(this.process) |
items.forEach(x => this.process(x)) |
this — they look up the scope chain for this just like any other variable, finding the value from the nearest enclosing regular function or class method. This captured value never changes regardless of how the arrow is called later. That is why arrows are the solution to this loss in callbacks inside class methods.user.getName() sets this to user. But const fn = user.getName; fn(); — there is no object before the dot, so this gets default binding. The function is identical in both cases — only the calling convention changes.super, and blocks subclass override. Reserve arrow functions for callbacks inside class methods, not for the methods themselves.Basic Example
// ── Rule 3: Implicit binding — object before the dot ─────────────────────
const person = {
name: 'Alice',
greet() { console.log(`Hi, I'm ${this.name}`); },
};
person.greet(); // 'Hi, I'm Alice' — person is before the dot
// ── Rule 4: Default binding — plain call loses context ───────────────────
const greet = person.greet;
// greet(); // In strict mode: TypeError (this is undefined)
// ── Rule 2: call and apply ────────────────────────────────────────────────
function introduce(greeting, punct) {
console.log(`${greeting}, I'm ${this.name}${punct}`);
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
introduce.call(alice, 'Hello', '!'); // Hello, I'm Alice!
introduce.apply(bob, ['Hey', '...']); // Hey, I'm Bob...
// ── bind — permanently bound function ────────────────────────────────────
const greetAlice = introduce.bind(alice, 'Welcome');
greetAlice('!'); // Welcome, I'm Alice!
greetAlice('.'); // Welcome, I'm Alice. — first arg pre-filled
// ── Rule 1: new binding ───────────────────────────────────────────────────
function User(name, role) {
this.name = name;
this.role = role;
}
const u = new User('Alice', 'admin');
console.log(u.name); // 'Alice'
// ── Arrow inherits lexical this — solves callback problem ────────────────
class Timer {
constructor(label) {
this.label = label;
this.elapsed = 0;
}
start() {
// Arrow captures 'this' from start() — always the Timer instance
this._id = setInterval(() => {
this.elapsed++;
console.log(`${this.label}: ${this.elapsed}s`);
}, 1000);
}
stop() { clearInterval(this._id); }
}
// ── this in class — bind vs arrow wrapper ─────────────────────────────────
class EventHandler {
constructor(name) {
this.name = name;
this.clickCount = 0;
// Pre-bind for use as callback reference
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.clickCount++;
console.log(`${this.name} clicked ${this.clickCount} times`);
}
register(element) {
element.addEventListener('click', this.handleClick); // bound ref
element.addEventListener('mouseenter', e => this.onEnter(e)); // arrow wrapper
}
onEnter(e) { console.log(`${this.name}: mouse entered`); }
}
// ── Borrowing methods with call ───────────────────────────────────────────
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const asArray = Array.prototype.slice.call(arrayLike);
console.log(asArray); // ['a', 'b', 'c']
const nums = [5, 2, 9, 1, 7];
console.log(Math.max(...nums)); // 9 (modern spread)
console.log(Math.max.apply(null, nums)); // 9 (classic apply)
How It Works
Step 1 — this Is Set at Call Time for Regular Functions
this is not a stored property of the function — it is an implicit argument passed in at the moment of the call. person.greet() implicitly passes person as this. greet() with no object passes nothing — this is undefined in strict mode.
Step 2 — Detaching a Method Loses Its Context
const fn = person.greet copies the function reference without the association to person. When fn() is called as a plain function, there is no object before the dot — default binding applies. The function itself is identical; only the invocation pattern changed.
Step 3 — bind Permanently Locks this
fn.bind(obj) returns a new function where this is permanently set to obj — no matter how it is called later. This bound copy can be safely passed as a callback. bind can also pre-fill arguments: greetAlice = introduce.bind(alice, 'Welcome') pre-fills the first argument.
Step 4 — Arrow Functions Capture Lexical this
Arrow functions have no this of their own. They find this by walking up the scope chain, like any other variable. Inside a class method, the arrow captures the method’s this — the instance. When setInterval later calls the arrow, this is still the instance.
Step 5 — new Is the Highest Priority Binding
new User('Alice') creates a fresh empty object, sets its prototype to User.prototype, runs the constructor with this pointing to the new object, and returns it. Even a bind-created function cannot override the this set by new.
Real-World Example: Dropdown Component
// dropdown.js
class Dropdown {
#isOpen = false;
#items;
#element;
#onSelect;
constructor(element, items, onSelect) {
this.#element = element;
this.#items = items;
this.#onSelect = onSelect;
this.#render();
this.#attachEvents();
}
#render() {
this.#element.innerHTML = `
<button class="toggle">Select...</button>
<ul class="menu" hidden>
${this.#items.map((item, i) =>
`<li data-index="${i}">${item.label}</li>`
).join('')}
</ul>`;
}
#attachEvents() {
// Arrow functions: 'this' stays as the Dropdown instance in all callbacks
this.#element.querySelector('.toggle').addEventListener('click', () => {
this.#isOpen ? this.#close() : this.#open();
});
this.#element.querySelector('.menu').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
this.#select(this.#items[Number(li.dataset.index)]);
});
document.addEventListener('click', (e) => {
if (!this.#element.contains(e.target)) this.#close();
});
}
#open() { this.#isOpen = true; this.#element.querySelector('.menu').hidden = false; }
#close() { this.#isOpen = false; this.#element.querySelector('.menu').hidden = true; }
#select(item) {
this.#element.querySelector('.toggle').textContent = item.label;
this.#onSelect?.(item);
this.#close();
}
}
Common Mistakes
Mistake 1 — Passing class method as callback without binding
❌ Wrong — this is lost:
['a','b','c'].forEach(logger.log); // TypeError — this is undefined
✅ Correct — bind or use arrow wrapper:
['a','b','c'].forEach(logger.log.bind(logger));
['a','b','c'].forEach(x => logger.log(x));
Mistake 2 — Arrow function as object method
❌ Wrong — this is not obj:
const obj = { value: 42, get: () => this.value };
console.log(obj.get()); // undefined
✅ Correct — use method shorthand:
const obj = { value: 42, get() { return this.value; } };
console.log(obj.get()); // 42
Mistake 3 — Regular function inside class method loses this
❌ Wrong:
start() {
setInterval(function() { this.count++; }, 1000); // TypeError
}
✅ Correct — use arrow:
start() {
setInterval(() => { this.count++; }, 1000); // correct
}
Quick Reference
| How Called | this Value |
|---|---|
new Fn() |
New empty object |
fn.call(obj, args) |
obj |
fn.apply(obj, [args]) |
obj |
fn.bind(obj)() |
obj — permanently bound |
obj.method() |
obj |
fn() plain call |
undefined (strict) / window (sloppy) |
Arrow () => {} |
Lexical — from enclosing scope, never changes |