this Binding in JavaScript
The four call-site rules that decide what this is, why arrow functions ignore them, how methods lose this when passed around, and how to get it back.
Unlike every other name in a function, this is not resolved by where the function was written but by how it was called. Four call forms cover everything: called with new, called through call/apply/bind, called as a method on an object, or called plainly. Arrow functions are the one exception: they have no this of their own and read the enclosing scope's, like any other variable. Every "lost this" bug is a function that was written as a method and then called some other way.
Context
In Java and C++ this always means "the instance this method belongs to", because a method cannot exist apart from its class. JavaScript (1995) made functions first-class values that can be attached to any object, detached again and passed around, so the same function can be invoked as a method of one object, as a callback with no object, or as a constructor. this had to be decided per call. In the original sloppy mode a plain call got the global object, which turned typos into global variables; ES5 strict mode (2009) changed that to undefined and added Function.prototype.bind. ES2015 added arrow functions, which fixed the callback case by not having a this at all, and classes; ES2022 class fields made handleClick = () => {…} the idiomatic way to keep a method bound.
You have met the problem as TypeError: Cannot read properties of undefined (reading 'setState') in a React class component, as this.handleClick = this.handleClick.bind(this) in a constructor, as $(this) in jQuery handlers, and as a Vue options-API component where this.count works in a method but not inside an arrow you wrote by habit. The simplest example is two lines:
const user = {name: 'Ada', greet() { return 'Hi, ' + this.name }}
user.greet() // 'Hi, Ada' — called as a method: this = user
const f = user.greet
f() // TypeError in strict mode: this is undefined
// 'Hi, undefined' in sloppy mode: this is globalThis- Call site
- The place a function is invoked, and the syntax used there. It, not the definition, decides this.
- Receiver
- The object before the dot in obj.method(). It becomes this for that call.
- Binding
- The association between this and a value for one invocation. Hard binding (bind) fixes it permanently.
- Strict mode
- Opt-in since ES5 with
'use strict'; always on in modules and classes. Plain calls getthis = undefinedinstead of the global object. - Arrow function
- A function with no own this, arguments, super or new.target. Reads this from the scope it was defined in.
Why it matters
Modern code avoids most this by using closures and hooks, but you still meet it everywhere the platform or a library hands you an object: DOM event handlers, Node EventEmitter listeners, Express middleware on a class, Mongoose schema methods, Vue's options API, and every class-based codebase written between 2015 and 2020. The failure is silent in sloppy mode and a confusing TypeError in strict mode, and the fix is trivial once you can say which of the four rules applied at the call site.
The four rules, in order of precedence
'use strict'
function show() { return this?.tag ?? this }
const a = {tag: 'a', show}
const b = {tag: 'b'}
show() // undefined rule 4: plain call
a.show() // 'a' rule 3: receiver is a
show.call(b) // 'b' rule 2: explicit
const bound = show.bind(b)
bound() // 'b' rule 2: hard-bound
a.show.call(b) // 'b' explicit beats implicit
new bound() // the new object rule 1 beats bind
;(0, a.show)() // undefined comma strips the receiver
const arrow = () => this
arrow.call(b) // module/script this arrows ignore call()Losing this, and getting it back
A method loses this the moment it is separated from its object: assigned to a variable, passed as a callback, destructured. At the eventual call site there is nothing before the dot, so rule 4 applies. setTimeout, addEventListener, map, promise then and React props all call your function plainly (or with a receiver of their own choosing).
class Counter {
count = 0
tick() { this.count++ ; render(this.count) }
}
const c = new Counter()
setTimeout(c.tick, 1000) // this = undefined → TypeError
button.addEventListener('click', c.tick) // this = button → button.count++
;[1, 2, 3].forEach(c.tick) // this = undefined → TypeError
const {tick} = c; tick() // same// 1. wrap in an arrow at the call site: the arrow closes over c
setTimeout(() => c.tick(), 1000)
// 2. bind once, keep the bound function (needed to remove the listener)
const tick = c.tick.bind(c)
button.addEventListener('click', tick)
button.removeEventListener('click', tick) // works: same reference
// 3. define the method as a class field arrow: bound per instance
class Counter {
count = 0
tick = () => { this.count++ ; render(this.count) }
}
setTimeout(new Counter().tick, 1000) // fine| Fix | Cost | Use when |
|---|---|---|
| Arrow wrapper at the call site | A new function per call site; clearest intent | One-off callbacks, timers, promise chains |
bind in the constructor | One bound function per instance; the original stays on the prototype | You must pass the same reference twice (add/remove listener), or support older code |
| Class field arrow | One function per instance, not on the prototype; cannot be overridden via super or spied on the prototype | Handlers that will always be passed around, e.g. React class components |
Pass thisArg (forEach, map) | Free, but only where the API offers it | Array iteration helpers with a method callback |
DOM handlers: which this do you want?
button.addEventListener('click', function () {
this.classList.toggle('on') // regular function: this = event.currentTarget
})
button.addEventListener('click', () => {
this.classList.toggle('on') // arrow: this = enclosing scope, NOT the button
})
button.addEventListener('click', e => {
e.currentTarget.classList.toggle('on') // explicit: works in either style
})Arrow functions and classes
An arrow function does not have this, arguments, super or new.target; a reference to any of them is resolved lexically, exactly like a variable, through the scope chain. That makes arrows perfect for callbacks inside methods and wrong for anything that is supposed to receive this from a caller.
const timer = {
seconds: 0,
start: () => {
// arrow as a METHOD: this is the module's this (undefined), not timer
setInterval(() => this.seconds++, 1000) // TypeError
},
}
class Api {
base = '/v1'
static create = () => new this() // ok: static field arrow, this = Api
}
Api.prototype.get = () => this.base // wrong: arrows have no per-call thisconst timer = {
seconds: 0,
start() { // method: this = timer
setInterval(() => this.seconds++, 1000) // arrow inherits it
},
}
// the pre-2015 version of the same idea, still in older codebases:
// start: function () { var self = this; setInterval(function () {
// self.seconds++ }, 1000) }
class Api {
base = '/v1'
get(path) { return fetch(this.base + path) } // regular method
}| Where | Regular function this | Arrow function this |
|---|---|---|
| Object literal property | The object, when called as obj.fn() | The this of the surrounding code (usually undefined in a module) |
| Class method vs class field arrow | Depends on the call site; shared on the prototype | Always the instance; one copy per instance |
| Callback inside a method | undefined / the caller’s choice | The method’s this: the common reason to use an arrow |
| Top level of a module | n/a | undefined. In a classic script it is window; in CommonJS it is module.exports. |
| Static method / static field | The class (constructor function) | The class |
call, apply and bind
call and apply invoke immediately with a chosen this (arguments listed versus in an array); bind returns a new function with this and optionally some leading arguments fixed. A bound function cannot be re-bound, and only new overrides it. Implementing bind is a common exercise because it uses all four rules at once.
Function.prototype.myBind = function (thisArg, ...preset) {
const target = this // the function being bound
function bound(...args) {
// called with new? then ignore thisArg (rule 1 beats rule 2)
const receiver = new.target ? this : thisArg
return target.apply(receiver, [...preset, ...args])
}
bound.prototype = Object.create(target.prototype) // so new bound() works
return bound
}
const greet = function (greeting, punct) {
return greeting + ', ' + this.name + punct
}
const hiAda = greet.myBind({name: 'Ada'}, 'Hi')
hiAda('!') // 'Hi, Ada!'
hiAda.call({name: 'Bob'}, '?') // 'Hi, Ada?' — cannot be re-bound
// method borrowing, the classic use of call/apply:
Array.prototype.slice.call(arguments) // pre-ES6 arguments → array
Math.max.apply(null, [3, 9, 2]) // pre-spread: Math.max(...arr)Pitfalls
- Arrow functions as object-literal or prototype methods
The arrow closes over the
thisof the code that built the object, which is not the object. In a module that isundefined, sothis.xthrows. Methods that need the receiver must be regular functions or method shorthand; use arrows for the callbacks inside them. - Binding a new function on every render or every add
onClick={this.handle.bind(this)}andaddEventListener('click', this.handle.bind(this))create a fresh function each time, so a laterremoveEventListenerwith anotherbindcall never matches, and memoised children re-render. Bind once, in the constructor or as a class field. - Class field arrows everywhere
They allocate one function per instance instead of one on the prototype, cannot be called through
super, and are invisible to code that spies on or patches the prototype (test mocks, decorators). Use them for handlers that get passed around; keep ordinary methods as methods. - Relying on sloppy-mode globalThis
Code that "worked" because a plain call got the global object, and wrote
this.cache = …ontowindow, breaks the moment it is moved into a module or a class, where strict mode is automatic. Treat plain-callthisas always undefined. - Expecting a closure to carry this
Only arrows capture
thislexically. A nestedfunctioninside a method gets its ownthisfrom its own call site, which is why thevar self = thisidiom existed. If a callback must be a regular function (to receive the DOM element, for example), read the outer object from a variable instead.
Interview questions
Q1What determines the value of this?
The call site, checked in order: a call with new binds this to the new object; call, apply or a bound function bind it to the object given; a call through a property, obj.fn(), binds it to obj; a plain call leaves it undefined in strict mode or the global object in sloppy mode. Arrow functions are outside the rules entirely: they have no own this and use the enclosing scope's, fixed at definition.
Q2const f = obj.method; f() — what happens and why?
The function runs with this undefined (or the global object in sloppy code), so any this.property access fails or reads a global. Assigning the method to a variable copies the function reference only; the object is not part of the function. At the call f() there is no receiver, so the plain-call rule applies. Fix with obj.method(), a bound copy, or an arrow wrapper.
Q3setTimeout(this.tick, 1000) inside a class breaks. Give three fixes and their trade-offs.
Wrap it: setTimeout(() => this.tick(), 1000), simplest, allocates a closure per call. Bind it once in the constructor, this.tick = this.tick.bind(this), keeps one reference you can also remove as a listener, while the prototype method stays shared. Or declare tick as a class field arrow, which auto-binds per instance at the cost of one function per object and no prototype method to override or mock.
Q4How do arrow functions differ from regular functions with respect to this, and when is an arrow the wrong choice?
An arrow has no own this, arguments, super or new.target; it resolves this lexically from where it was written and ignores call, apply, bind and the receiver. That is what you want for a callback inside a method. It is wrong for an object-literal method, a prototype method, a constructor, or a DOM handler that relies on this being the element, because in all of those the caller is supposed to supply this.
Q5Implement Function.prototype.bind.
Return a function that captures the target and the fixed this and preset arguments in a closure, then calls target.apply with the preset plus the new arguments. Handle the new case: if the returned function is invoked with new, use the newly created object as this instead of the bound one, and set the returned function's prototype to inherit from the target's so instanceof works. A bound function cannot be re-bound because the closure ignores any later receiver.
Q6What is this inside a DOM event handler?
For a regular function, the element the listener was registered on, the same as event.currentTarget, because the browser calls the handler with that receiver. For an arrow function it is whatever this was in the surrounding scope, which in a module is undefined. Reading event.currentTarget explicitly works with either and is clearer.
Q7What does the new operator do, step by step?
It creates an empty object whose prototype is the function's prototype property, calls the function with this bound to that object and the given arguments, and returns the object unless the function itself returned a different object. Because new supplies the receiver, it takes precedence over bind, and it cannot be used with arrow functions or with methods defined by method shorthand, which have no construct behaviour.
thisis decided at the call site, in order:new, thencall/apply/bind, then the receiver before the dot, then plain call (undefined in strict mode).- Arrow functions have no own
this; they read the enclosing scope's and ignore call, apply and bind. Use them for callbacks inside methods, never as methods. - A method loses
thisas soon as it is passed as a value. Wrap it in an arrow, bind it once, or declare it as a class field arrow. - Bind once and keep the reference if you need to remove the listener later; class field arrows cost one function per instance and skip the prototype.
- In DOM handlers a regular function gets the element; reading
event.currentTargetworks in either style. newcreates the object, binds it asthis, and returns it; it overrides even a bound function.