Understanding the this keyword in JavaScript
this confuses more developers than any other part of JavaScript, because it doesn't
behave like a normal variable. The value of this isn't fixed where a function is
written — it's decided by how the function is called. Master the handful of rules
that govern it and the bugs (lost context, undefined in callbacks, surprising
globals) stop being mysterious. This guide covers the binding rules, arrow functions,
call/apply/bind, and the classic pitfalls.
The four binding rules
For a regular function, this is set by the call site according to four rules, in
order of precedence:
newbinding —new Fn()->thisis the brand-new instance.- Explicit binding —
fn.call(obj),fn.apply(obj),fn.bind(obj)-> the object you pass. - Implicit binding —
obj.fn()-> the object left of the dot. - Default binding — a plain
fn()->undefinedin strict mode, or the global object in sloppy mode.
function who() { return this }
const obj = { who }
obj.who() // obj (implicit)
who.call('hi') // 'hi' (explicit)
new who() // {} (new instance)
who() // undefined in strict mode
When several could apply, precedence decides: new beats bind beats implicit beats
default.
Arrow functions ignore all of that
Arrow functions don't have their own this. They capture it lexically — from
the enclosing scope at definition time — and it can never be reassigned, not even by
call/apply/bind. This makes them ideal for callbacks that need the surrounding
this.
class Timer {
seconds = 0
start() {
setInterval(() => this.seconds++, 1000) // arrow keeps `this` = the Timer
}
}
The flip side: arrows are a poor choice for object methods that reference the
object, because this points to the outer scope, not the object — and they can't be
constructors.
const obj = {
name: 'Ada',
greet: () => `Hi, ${this.name}`, // this is the outer scope -> undefined
greet2() { return `Hi, ${this.name}` }, // method shorthand -> 'Ada'
}
Arrows also lack their own arguments, super, and new.target.
Losing this: the most common bug
Because this is set at call time, passing a method somewhere else detaches it from
its object. The function is copied without the obj. context, so when it's later called
plainly, this is undefined/global.
const user = { name: 'Ada', greet() { return this.name } }
const fn = user.greet
fn() // undefined — detached
setTimeout(user.greet, 0) // also detached
The same happens with destructuring (const { greet } = user). Fixes: an arrow
wrapper that calls the method on the object, or bind:
setTimeout(() => user.greet(), 0) // called ON user
setTimeout(user.greet.bind(user), 0) // permanently bound
This is exactly why React class components needed
this.handleClick = this.handleClick.bind(this) in the constructor, or arrow class
fields.
call, apply, and bind
All three set this explicitly; they differ in invocation and arguments:
fn.call(thisArg, a, b)— invoke now, args listed.fn.apply(thisArg, [a, b])— invoke now, args as an array.fn.bind(thisArg, a)— return a new function withthis(and any given args) permanently locked in.
function greet(g, mark) { return `${g}, ${this.name}${mark}` }
const user = { name: 'Ada' }
greet.call(user, 'Hi', '!') // 'Hi, Ada!'
greet.apply(user, ['Hi', '!']) // 'Hi, Ada!'
const greetAda = greet.bind(user)
greetAda('Hey', '.') // 'Hey, Ada.'
bind also enables partial application (pre-fill leading args with a null
thisArg). Once bound, this is fixed — a later call/apply/bind can't change it.
But new does override a bound this (the new instance wins, though bound arguments
are kept). For just forwarding an array of arguments without caring about this, modern
code uses the spread operator (Math.max(...nums)) instead of apply.
this in different contexts
- Methods (shorthand or prototype):
thisis the object the method is called on. - Constructors /
new: a fresh object is created andthispoints to it; it's returned automatically unless the constructor returns its own object. Forgettingnewtriggers default binding and leaks globals. - Classes: bodies are always strict, so an extracted method called plainly has
this === undefined(aTypeError, not the global object). Arrow class fields bindthisto the instance. - Static methods:
thisis the class itself. - Event listeners: a regular-function handler's
thisis the element; an arrow's is the surrounding scope. - Top level: in a classic script (sloppy)
thisis the global object; in an ES module it'sundefined. UseglobalThisto reliably reference the global object anywhere.
class Counter {
count = 0
inc() { this.count++ }
incArrow = () => this.count++ // bound to the instance
}
Strict mode and method chaining
Strict mode makes a plain call's this undefined instead of the global object,
surfacing "I forgot to bind this" bugs immediately (a loud TypeError rather than
silent global leakage). ES modules and classes are always strict. A useful pattern that
relies on implicit binding: returning this from methods to enable fluent chaining:
class Query {
parts = []
where(c) { this.parts.push(c); return this }
order(o) { this.parts.push(o); return this }
}
new Query().where('a=1').order('b') // each call returns the same instance
Recap
this is determined by the call site, not the definition site, following four
rules: new, explicit (call/apply/bind), implicit (obj.method()), and default
(undefined/global). Arrow functions opt out and inherit this lexically — great
for callbacks, wrong for object methods. Passing a method as a callback loses its
receiver; fix it with an arrow wrapper or bind. Class bodies and modules are strict,
so detached methods get undefined. Internalize the rules and this becomes
predictable instead of magical.