this Binding Rules
The Four Rules of this
this is not a variable. It's a binding that the engine sets when a function is called, based on how the function was called — not where it was defined. This is the opposite of lexical scope, and it's why this confuses so many developers.
But there's good news: there are exactly four rules, and they have a strict priority order. Learn the priority, and you can predict this in any situation.
Think of this as a name tag that gets pinned to a function at the moment of calling. The name tag says "I belong to ___." The four rules determine who fills in the blank:
- new — "I belong to the brand new object"
- call/apply/bind — "I belong to whoever was explicitly specified"
- obj.method() — "I belong to the object before the dot"
- plain call — "I belong to nobody (undefined in strict mode, globalThis in sloppy mode)"
Check from top to bottom. The first rule that matches wins.
Rule 4 (Lowest Priority): Default Binding
When a function is called with no context — just fn():
function showThis() {
console.log(this);
}
showThis(); // In strict mode: undefined
// In sloppy mode: globalThis (window in browsers)
In strict mode (and all ES modules are strict), default this is undefined. In sloppy mode, it's the global object. This is why forgetting this in class methods results in "Cannot read property of undefined" in modern code.
Rule 3: Implicit Binding (The Dot Rule)
When a function is called as a method of an object — obj.fn() — this is the object before the dot:
const user = {
name: "Alice",
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
user.greet(); // "Hi, I'm Alice" — this = user (before the dot)
Only the last object in a chain matters:
const company = {
name: "Acme",
department: {
name: "Engineering",
getName() {
return this.name;
}
}
};
company.department.getName(); // "Engineering" — this = department (last before dot)
The Method Extraction Trap
This is the single most common this bug in JavaScript:
const user = {
name: "Alice",
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
// Extracting the method loses the implicit binding
const greetFn = user.greet;
greetFn(); // "Hi, I'm undefined" (strict) — this is now default binding!
// This happens constantly with callbacks:
setTimeout(user.greet, 100); // "Hi, I'm undefined"
button.addEventListener("click", user.greet); // this = button, not user
[1, 2].forEach(user.greet); // "Hi, I'm undefined"
Rule 2: Explicit Binding (call, apply, bind)
You can force this to a specific value:
function greet(greeting) {
console.log(`${greeting}, I'm ${this.name}`);
}
const user = { name: "Alice" };
// call — arguments listed individually
greet.call(user, "Hello"); // "Hello, I'm Alice"
// apply — arguments as an array
greet.apply(user, ["Hello"]); // "Hello, I'm Alice"
// bind — returns a NEW function with this permanently set
const boundGreet = greet.bind(user);
boundGreet("Hello"); // "Hello, I'm Alice"
bind creates a new function where this is permanently fixed. You can't override a bound function's this with call or apply:
const other = { name: "Bob" };
boundGreet.call(other, "Hey"); // "Hey, I'm Alice" — still Alice!
call and apply with null or undefined as the first argument fall back to default binding in sloppy mode (global object). In strict mode, this is literally null/undefined:
function showThis() { console.log(this); }
showThis.call(null);
// Sloppy mode: window
// Strict mode: nullThis is a security concern in sloppy mode — you might accidentally expose the global object. Always use strict mode.
Rule 1 (Highest Priority): new Binding
When a function is called with new, this is bound to a fresh empty object:
function User(name) {
// this = {} (brand new object, created by 'new')
this.name = name;
// return this (implicit)
}
const alice = new User("Alice");
alice.name; // "Alice"
new overrides even explicit binding:
function Foo(name) {
this.name = name;
}
const bound = Foo.bind({ name: "ignored" });
const obj = new bound("winner");
obj.name; // "winner" — new > bind
Arrow Functions — The Exception to Everything
Arrow functions do NOT have their own this. They capture this from their enclosing lexical scope at definition time. None of the four rules apply:
const user = {
name: "Alice",
greet: () => {
console.log(this.name); // this is from the outer scope, NOT user
},
delayedGreet() {
// Arrow function captures 'this' from delayedGreet's scope
setTimeout(() => {
console.log(this.name); // "Alice" — this = user (from the method call)
}, 100);
}
};
user.greet(); // undefined — arrow this = outer scope (module/global)
user.delayedGreet(); // "Alice" — arrow captures this from delayedGreet
greet: () => {} on an object means this is the enclosing scope (often global/module), not the object. This is a common React bug when migrating from class components: arrow methods in object literals don't bind to the object.
You cannot rebind an arrow function's this:
const arrow = () => this;
arrow.call({ name: "test" }); // Still the outer 'this' — call is ignored
arrow.bind({ name: "test" })(); // Still the outer 'this' — bind is ignored
new arrow(); // TypeError: arrow is not a constructor
The Priority Summary
new binding → this = new object (highest)
explicit binding → this = specified object
implicit binding → this = object before the dot
default binding → this = undefined (strict) / globalThis (sloppy)
arrow function → this = inherited from lexical scope (immutable)
Production Scenario: React Class Component Bug
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// Fix 1: bind in constructor
// this.tick = this.tick.bind(this);
}
tick() {
this.setState({ count: this.state.count + 1 }); // Bug: this is undefined
}
render() {
// tick is passed as a callback — method extraction trap
return <button onClick={this.tick}>Count: {this.state.count}</button>;
// When React calls the callback, it's a plain call: tick()
// Rule 4 applies: this = undefined (strict mode)
// Fix 2: arrow function in JSX
// return <button onClick={() => this.tick()}>Count</button>;
// Fix 3: class field with arrow (modern)
// tick = () => { this.setState(...) };
}
}
Why class fields with arrows work
Class fields like tick = () => {} are syntactic sugar for assignment in the constructor. They're equivalent to:
constructor(props) {
super(props);
this.tick = () => { /* ... */ };
}Since the arrow function is created inside the constructor, it captures this from the constructor's scope — which is always the instance. The downside: every instance gets its own copy of the function (can't be shared via prototype), which increases memory usage with many instances.
| What developers do | What they should do |
|---|---|
| Assuming this is determined by where a function is defined this binding is dynamic. Only arrow functions have lexical this. | this is determined by HOW the function is called (except arrow functions) |
| Using arrow functions as object methods Arrow functions capture this from the enclosing scope, not from the object they're on | Use regular functions for object methods. Use arrows for callbacks inside methods. |
| Passing methods as callbacks without binding Method extraction loses implicit binding — the callback becomes a plain function call | Use .bind(), arrow wrappers, or arrow class fields |
| Trying to override arrow function's this with call/apply/bind Arrow functions don't have their own this. call/apply/bind have no effect on their this. | Arrow function this is permanently fixed at definition time |
- 1this is determined at call time by four rules in priority order: new > explicit (call/apply/bind) > implicit (dot) > default (plain call)
- 2Arrow functions have no own this — they inherit it from the enclosing lexical scope at definition time, permanently
- 3Method extraction (passing obj.method as a callback) loses implicit binding — this becomes default
- 4bind creates a permanently bound function that can only be overridden by new
- 5In strict mode (and all modules), default this is undefined. In sloppy mode, it's globalThis.