JavaScript's this: who is it actually pointing at?
A walkthrough of the four binding rules for this and the priority order between them, plus why arrow functions don't get to decide their own this — so you can look at any snippet and know exactly what this points to.
Why does this trip everyone up?
Nearly everyone who's learned JavaScript has gotten burned by this at some point. Take the exact same code, call it from a different place, and suddenly this points somewhere else entirely.
The root cause is a fact that's easy to overlook: this isn't decided when a function is defined — it's decided the moment the function is called.
Let's look at an example
const person = {
name: 'Alice',
greet() {
console.log(this.name)
},
}
person.greet() // Alice
const fn = person.greet
fn() // undefined (undefined in strict mode; window.name in non-strict mode)Same greet function. Call it as person.greet() and this points to person. Assign it to fn and call it from there, and this no longer points to person. The function itself hasn't changed at all — only how it was called. That's the core of why this is dynamically bound.
The four binding rules
1. Default binding
Call a function directly, with no object in front of it, and it falls back to default binding. In strict mode, this is undefined. In non-strict mode, it's the global object (window in browsers).
function showThis() {
console.log(this)
}
showThis() // strict mode: undefined; non-strict mode: window2. Implicit binding
When you call a function through an object, this binds to whatever object sits right before the dot at the call site.
const dog = {
name: 'Bob',
bark() {
console.log(`${this.name}: Woof!`)
},
}
dog.bark() // Bob: Woof!That fn() from earlier came out undefined precisely because the call no longer had person. in front of it. Strip away the object and implicit binding goes with it — you're back to default binding.
3. Explicit binding
call(), apply(), and bind() let you specify exactly which object this should bind to, skipping the whole "infer it from how it's called" step.
function introduce() {
console.log(`I'm ${this.name}`)
}
const cat = { name: 'Cathy' }
introduce.call(cat) // I'm Cathy
introduce.apply(cat) // I'm Cathy
const boundIntroduce = introduce.bind(cat)
boundIntroduce() // I'm Cathy — locked to cat no matter how you call it from here oncall() and apply() only differ in how you pass arguments — one takes them comma-separated, the other as an array — but they behave the same way. bind() is different: it returns a brand-new function with this permanently locked in, and no later call can change that.
4. new binding
Call a constructor with new and JavaScript automatically creates a fresh object, then binds this to it.
function Person(name) {
this.name = name
}
const p = new Person('David')
console.log(p.name) // DavidWhen multiple rules apply, who wins?
If a single call could satisfy more than one of these rules at once, here's the priority order, highest to lowest:
- new binding — called with
new,thisis always the newly created object. - Explicit binding — set explicitly via
call,apply, orbind. - Implicit binding — called through an object, like
obj.method(). - Default binding — none of the above apply, so it falls back to
undefined(strict mode) or the global object.
Arrow functions are the exception — they don't have their own this
All four rules above only apply to regular functions — function declarations or function expressions. Arrow functions sit outside this system entirely: they don't have their own this. Instead, they inherit this from whatever scope they were defined in.
const team = {
name: 'Team A',
showMembers() {
const members = ['X', 'Y']
members.forEach(function (member) {
console.log(`${this.name}: ${member}`) // this isn't team — it's undefined or window
})
},
}
team.showMembers()Here's the trap: the callback inside forEach is a regular function, and when it's called there's no object in front of it, so this falls back to default binding — completely disconnected from the outer team. Switching to an arrow function fixes it:
const team = {
name: 'Team A',
showMembers() {
const members = ['X', 'Y']
members.forEach((member) => {
console.log(`${this.name}: ${member}`) // Team A: X → Team A: Y
})
},
}
team.showMembers()The arrow function is defined inside showMembers, so its lexical scope is showMembers. That means its this just reuses whatever this was when showMembers got called (team, in this case) — it doesn't care at all how the arrow function itself gets invoked. This is also why you can't change an arrow function's this with call, apply, or bind: there's no this of its own to bind in the first place.
Common pitfalls
Passing a method off as a callback
class Timer {
constructor() {
this.seconds = 0
}
tick() {
this.seconds++
console.log(this.seconds)
}
}
const timer = new Timer()
setTimeout(timer.tick, 1000) // NaN or a crash — this is no longer timertick gets handed to setTimeout on its own, so by the time it's called there's no timer. prefix left — it falls back to default binding, and this.seconds has nothing to read from. There are two fixes: lock this with bind, or wrap the call in an arrow function.
setTimeout(timer.tick.bind(timer), 1000)
// or
setTimeout(() => timer.tick(), 1000)this inside event handlers
Use a regular function as an event handler and this points to the DOM element that triggered the event. Use an arrow function instead, and this comes from whatever scope enclosed it at definition time. These two behave differently, and mixing them is an easy way to get burned — know which one you're reaching for.
Wrapping up
this looks unpredictable, but it actually follows clear rules. For regular functions, this is decided by how the function is called, with new binding > explicit binding > implicit binding > default binding as the priority order. Arrow functions skip that system entirely and just inherit this from their enclosing scope at definition time. Next time this isn't what you expected, ask yourself one question: "How was this function called?" The answer is almost always right there.