The this Keyword

▶ Try It Yourself

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))
Note: Arrow functions do not have their own 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.
Tip: The key mental model for implicit binding is “the object before the dot.” 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.
Warning: Using arrow functions as class methods (class fields) creates a new function per instance as an own property — not a shared prototype method. This uses more memory, prevents method overriding with 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
}

▶ Try It Yourself

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

🧠 Test Yourself

What is this inside an arrow function used as a setInterval callback inside a class method?





▶ Try It Yourself