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 asobj.regular()— implicit binding,thisisobj, returns"Alice".obj.arrow(): arrow function —thisis lexical (the enclosing scope'sthis).objis an object literal, not a function scope, so the arrow inherits the module scope'sthis(undefined in strict mode, global in sloppy).this.nameisundefined.obj.bound(): the function expression(function() { return this.name; })is immediately.bind({ name: "Bob" }). The bind permanently setsthisto{ name: "Bob" }. When called asobj.bound(), the bind overrides implicit binding —thisis{ 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).