11 — this Binding Rules: Call Site, Arrow Lexical & Explicit Binding

The Four Binding Rules

javascript
// ── `this` is determined by the CALL SITE (how the function is called), not the definition ──
// Four rules, in order of precedence (highest wins):
// 1. `new` binding: this = the new object being created
// 2. Explicit binding: call/apply/bind — this = the first argument
// 3. Implicit binding: obj.method() — this = obj (the receiver before the dot)
// 4. Default binding: this = global (or undefined in strict mode)

// ── Rule 4: Default binding (standalone function call) ──
function showThis() { console.log(this); }
showThis();  // global object (window/globalThis) in sloppy mode; undefined in strict mode
// `this` is the global object because there's no receiver (no obj.method() pattern)

// ── Rule 3: Implicit binding (method call — the receiver) ──
const obj = {
    name: "Alice",
    greet() { console.log(`Hello, ${this.name}`); }
};
obj.greet();  // "Hello, Alice" — `this` is obj (the receiver before the dot)
// The call site is `obj.greet()` — `obj` is the implicit `this`

// ── ⚠️ Implicit binding is LOST when the method is detached ──
const greet = obj.greet;  // assign the method to a variable (detached from obj)
greet();  // "Hello, undefined" — `this` is the global/undefined (default binding!)
// The call site is `greet()` (no receiver) → default binding
// This is the #1 `this` bug: passing a method as a callback loses its receiver.

// ── Rule 2: Explicit binding (call/apply/bind) ──
function greet2(greeting) { return `${greeting}, ${this.name}`; }
const user = { name: "Bob" };
greet2.call(user, "Hi");     // "Hi, Bob" — call: this=user, args passed individually
greet2.apply(user, ["Hi"]);  // "Hi, Bob" — apply: this=user, args as array
const bound = greet2.bind(user);  // bind: creates a NEW function with this permanently set
bound("Hello");  // "Hello, Bob" — this is always user (can't be changed)

// ── Rule 1: `new` binding (constructor call) ──
function Person(name) {
    this.name = name;  // `this` is the newly created object
}
const alice = new Person("Alice");
console.log(alice.name);  // "Alice" — `this` was the new object created by `new`
// `new` creates a fresh object, calls Person with `this` = that object, returns it

Arrow Functions: Lexical this (Not Affected by the Four Rules)

javascript
// ── Arrow functions DON'T have their own `this` — they inherit from the enclosing scope ──
// The four binding rules do NOT apply to arrows.
// Arrow `this` is determined at DEFINITION time (lexical), not at call time (dynamic).

const obj = {
    name: "Alice",
    // Regular function: dynamic `this` (rule 3 — implicit binding)
    regularGreet() { console.log(`Hello, ${this.name}`); },

    // Arrow function: lexical `this` (inherits from obj's scope → the module scope)
    arrowGreet: () => console.log(`Hello, ${this.name}`),  // this.name is undefined!
    // The arrow's `this` is the enclosing scope's `this` — which is the module/global scope.
    // The module scope's `this` is undefined (strict mode) or global object (sloppy).
    // obj is NOT the enclosing scope — obj is an object literal, not a function scope.
};

obj.regularGreet();  // "Hello, Alice" — this = obj (implicit binding)
obj.arrowGreet();    // "Hello, undefined" — this = enclosing scope (lexical, NOT obj)

// ── Where arrows ARE useful: callbacks inside methods ──
const counter = {
    count: 0,
    increment() {
        // `this` here is counter (implicit binding via method call)
        [1, 2, 3].forEach(() => {
            this.count++;  // ✓ arrow inherits `this` from increment() → counter
            // If this were a regular function, `this` would be lost (default binding)
        });
    },
};
counter.increment();
console.log(counter.count);  // 3

// ── Arrow `this` can't be overridden by call/apply/bind ──
const arrow = () => console.log(this);
arrow.call({ name: "forced" });  // `this` is STILL the lexical scope (ignored call's first arg)
// Arrows ignore explicit binding — their `this` is permanently lexical.

Anti-Pattern: Lost this in Callbacks

javascript
// ❌ NAIVE — regular function as callback loses `this`
class Timer {
    constructor() {
        this.seconds = 0;
    }
    start() {
        setInterval(function() {
            this.seconds++;  // ✗ `this` is NOT the Timer instance (default binding → global)
            // In a regular function passed to setInterval, `this` is the global object
            console.log(this.seconds);  // NaN (global.seconds is undefined → undefined+1 = NaN)
        }, 1000);
    }
}
// new Timer().start();  // logs NaN every second (this.seconds is undefined)

// ✅ CORRECT — arrow function preserves `this` (lexical binding)
class Timer2 {
    constructor() { this.seconds = 0; }
    start() {
        setInterval(() => {
            this.seconds++;  // ✓ `this` is the Timer2 instance (lexical from start())
            console.log(this.seconds);
        }, 1000);
    }
}

// ✅ ALSO CORRECT — bind the regular function
class Timer3 {
    constructor() { this.seconds = 0; }
    start() {
        setInterval(function() {
            this.seconds++;
        }.bind(this), 1000);  // bind creates a new function with `this` permanently set
    }
}

// ✅ ALSO CORRECT — save `this` to a variable (pre-ES6 pattern)
class Timer4 {
    constructor() { this.seconds = 0; }
    start() {
        const self = this;  // save reference to `this`
        setInterval(function() {
            self.seconds++;  // use self (closure captures the outer this)
        }, 1000);
    }
}

call, apply, and bind Internals

javascript
// ── call: invoke with this and individual args ──
function introduce(greeting, punctuation) {
    return `${greeting}, I'm ${this.name}${punctuation}`;
}
introduce.call({ name: "Alice" }, "Hello", "!");  // "Hello, I'm Alice!"

// ── apply: invoke with this and args as array ──
introduce.apply({ name: "Bob" }, ["Hi", "?"]);  // "Hi, I'm Bob?"

// ── bind: create a new function with permanent this (and optional pre-filled args) ──
const aliceIntroduce = introduce.bind({ name: "Charlie" }, "Hey");
aliceIntroduce(".");  // "Hey, I'm Charlie." (greeting pre-filled, punctuation passed at call)
// bind doesn't invoke immediately — it returns a new function

// ── Partial application with bind (pre-fill args) ──
function add(a, b, c) { return a + b + c; }
const add5 = add.bind(null, 5);  // first arg = 5 (null for this — don't care)
add5(10, 20);  // 35 (5 + 10 + 20)

// ── Borrowing methods (use another object's method on your object) ──
const arrayLike = { 0: "a", 1: "b", length: 2 };
// arrayLike doesn't have .join, but Array.prototype does:
Array.prototype.join.call(arrayLike, "-");  // "a-b" (borrows Array.join, this = arrayLike)

// ── Hard binding (bind can't be undone) ──
const bound = introduce.bind({ name: "Hard" });
// bound.call({ name: "Override" });  // `this` is STILL { name: "Hard" } (bind is permanent)
// bind creates a wrapper function that ignores the call's `this` and uses the bound one.

Production Pattern: Event Handler this Management

javascript
// ── In event handlers, `this` is the element that triggered the event ──
// ── Arrow function event handlers: `this` is NOT the element (lexical!) ──

class ButtonController {
    constructor(button) {
        this.button = button;
        this.clickCount = 0;

        // ❌ Regular function: `this` is the button element (not the controller)
        // this.button.addEventListener("click", function() {
        //     this.clickCount++;  // ✗ this is the button, not the controller
        // });

        // ✅ Arrow function: `this` is the controller (lexical)
        this.button.addEventListener("click", () => {
            this.clickCount++;  // ✓ this is the controller
            this.updateLabel();
        });

        // ✅ Alternative: bind the regular function
        this.button.addEventListener("click", this.handleClick.bind(this));
    }

    handleClick() {
        this.clickCount++;  // this is the controller (bound)
        // Note: `this.button` inside is the controller's button, not the event target
        // If you need the event target: use event.currentTarget
    }

    updateLabel() {
        this.button.textContent = `Clicked ${this.clickCount} times`;
    }
}

// ── When you DO need `this` to be the element (e.g., accessing dataset) ──
document.querySelectorAll(".item").forEach(function(el) {
    // Regular function in forEach: `this` is undefined/global (NOT el)
    el.addEventListener("click", function(event) {
        // `this` is the clicked element (event handler binding)
        console.log(this.dataset.id);  // ✓ this is the element
    });
});

💡 Tips & Tricks

javascript
// ── `this` in class fields: arrow vs regular method ──
class Counter {
    count = 0;

    // Regular method: `this` is dynamic (depends on call site)
    increment() { this.count++; }

    // Class field arrow: `this` is bound to the instance (lexical, permanent)
    // Useful for callbacks: `button.onclick = counter.incrementArrow`
    incrementArrow = () => { this.count++; };
}
const c = new Counter();
const detached = c.increment;
// detached();  // ✗ TypeError: this is undefined (lost the receiver)
const detachedArrow = c.incrementArrow;
detachedArrow();  // ✓ works (arrow has permanent this = c)

// ── `this.constructor` for accessing the class from an instance ──
class Animal {
    reproduce() { return new this.constructor(); }  // creates same type as the instance
}
class Dog extends Animal {}
const puppy = new Dog().reproduce();  // puppy is a Dog (this.constructor = Dog)

// ── `new.target` for detecting constructor calls ──
function Base() {
    if (!new.target) throw new Error("must call with new");
    // new.target is undefined if called without `new`
    // new.target is the constructor if called with `new`
}
// Base();  // throws
new Base();  // works

// ── Avoid `this` entirely with arrow functions and closures ──
const createCounter = () => {
    let count = 0;  // private state (closure)
    return {
        increment: () => ++count,   // arrow — no `this` needed
        get count() { return count; },  // getter
    };
};

⚠️ Edge Cases & Gotchas

javascript
// ── `this` in a standalone regular function is global (sloppy) or undefined (strict) ──
function test() { return this; }
test();  // window/globalThis (sloppy), undefined (strict)

// ── `this` in an arrow function is the ENCLOSING scope's `this` ──
const obj = { method: () => this };
obj.method();  // `this` is the module scope (NOT obj — arrows don't get implicit binding)

// ── `this` in a class constructor is the new instance ──
class C { constructor() { console.log(this); } }
new C();  // C {} (the new instance)

// ── `this` in static methods is the class itself ──
class D {
    static method() { console.log(this); }
}
D.method();  // class D (this = the class, not an instance)

// ── Implicit binding is lost when detached (passed as callback) ──
const obj = { greet() { console.log(this); } };
setTimeout(obj.greet, 0);  // `this` is global/undefined (detached — no receiver)

// ── `bind` is permanent (can't be overridden) ──
const bound = obj.greet.bind({ name: "bound" });
bound.call({ name: "override" });  // `this` is STILL { name: "bound" }

// ── Arrow function `this` can't be changed ──
const arrow = () => console.log(this);
arrow.call({ forced: true });  // `this` is still the lexical scope (call ignored)

// ── `this` in `setTimeout` callback is global (not the enclosing object) ──
const obj2 = {
    delay: 1000,
    run() {
        setTimeout(function() {
            // `this` here is the global object, NOT obj2
            // console.log(this.delay);  // undefined
        }, this.delay);  // ✓ this is obj2 here (in the run() method scope)
    },
};

🧠 Quick Quiz

What does this output?

javascript
const obj = {
    name: "Alice",
    regular() { return this.name; },
    arrow: () => this.name,
    bound: (function() { return this.name; }).bind({ name: "Bob" }),
};
console.log(obj.regular());
console.log(obj.arrow());
console.log(obj.bound());
Answer
"Alice"   // regular(): implicit binding — this = obj (obj.regular())
"undefined" // arrow(): lexical `this` — the module/global scope (NOT obj)
"Bob"     // bound(): the function was bound to { name: "Bob" } at definition time
  • obj.regular(): called as obj.regular() — implicit binding, this is obj, returns "Alice".
  • obj.arrow(): arrow function — this is lexical (the enclosing scope's this). obj is an object literal, not a function scope, so the arrow inherits the module scope's this (undefined in strict mode, global in sloppy). this.name is undefined.
  • obj.bound(): the function expression (function() { return this.name; }) is immediately .bind({ name: "Bob" }). The bind permanently sets this to { name: "Bob" }. When called as obj.bound(), the bind overrides implicit binding — this is { name: "Bob" }, returns "Bob".

The lesson: regular functions get this from the call site (dynamic). Arrow functions get this from the definition scope (lexical). Bound functions have this permanently set (bind is irreversible).