Concept
The beginner framing: this is a special keyword inside a function that refers to "the object the function is being run as a method of", but which object that actually is depends entirely on how you call the function, not where you wrote it.
The precise mental model: unlike every other binding you've seen so far, this is not resolved via the scope chain. It's a special value set fresh on the execution context (see Execution Context) every time a regular function is called, and the value depends on the call site, the exact expression used to invoke the function. There are four rules, and you apply them in this precedence order (highest wins):
newbinding,new Fn()→thisis the newly created object.- Explicit binding,
fn.call(obj),fn.apply(obj), or a function created withfn.bind(obj)→thisis exactlyobj, no matter how it's later invoked. - Implicit binding,
obj.method()→thisisobj, the object immediately to the left of the dot at the call site. - Default binding, a bare
fn()call with no object context →thisisundefinedin strict mode (or the global object in non-strict/sloppy mode).
The engine-level view: arrow functions are the one big exception to all of this. An arrow function has no this of its own, it doesn't get a this binding set on its execution context at all. Instead, any this reference inside an arrow function is resolved via the normal scope chain, exactly like any other variable, walking outward to find the nearest enclosing this, captured lexically, at the arrow function's definition site, permanently.
const obj = {name: "obj",regular() { return this.name; },arrow: () => this.name,};const fn = obj.regular;obj.regular();fn();obj.regular.call({ name: "custom" });obj.arrow();
Called as obj.method() → this = obj. This is implicit binding, the most common case.
const obj = {
name: "obj",
regular() { return this.name; },
arrow: () => this.name,
};
const fn = obj.regular;
obj.regular(); // "obj", implicit binding
fn(); // undefined, default binding (lost the obj. prefix)
obj.regular.call({ name: "custom" }); // "custom", explicit binding
obj.arrow(); // undefined, arrow's `this` is the outer (module) scope'sExplicit binding: call, apply, and bind
function introduce(greeting) {
return `${greeting}, I'm ${this.name}`;
}
const person = { name: "Sam" };
introduce.call(person, "Hi"); // .call, args passed individually
introduce.apply(person, ["Hi"]); // .apply, args passed as an array
const bound = introduce.bind(person);
bound("Hi"); // .bind, returns a NEW function, permanently boundbind is the only one of the three that doesn't call the function immediately, it returns a new function with this (and optionally some leading arguments) permanently locked in, immune to being re-bound by implicit or default binding later.
new binding, briefly
function Person(name) {
this.name = name; // `this` here is the brand-new object `new` just created
}
const alice = new Person("Alice");
alice.name; // "Alice"new overrides every other rule, even if Person were called as obj.Person(...), using new still creates a fresh object and binds this to it.
Try It
Predict each this value before running:
const timer = {
seconds: 0,
startRegular() {
setInterval(function () {
this.seconds++; // regular function passed to setInterval
console.log(this.seconds);
}, 1000);
},
startArrow() {
setInterval(() => {
this.seconds++; // arrow function
console.log(this.seconds);
}, 1000);
},
};Solution
startRegular()'s callback logs NaN repeatedly (this is undefined/the global object inside the callback, setInterval calls it as a bare function, default binding, so this.seconds is either a crash in strict mode or undefined + 1 = NaN in sloppy mode).
startArrow()'s callback correctly increments and logs 1, 2, 3, ..., the arrow function has no this of its own, so it uses startArrow's this, which is timer (implicit binding from timer.startArrow()).
This exact pattern, losing in a callback passed to a timer, event listener, or array method, is the single most common bug in real code, and it's why arrow functions became the default choice for callbacks.
Implement It Yourself
Implement a simplified version of Function.prototype.bind to see exactly what it does:
function myBind(fn, boundThis, ...boundArgs) {
return function (...callArgs) {
return fn.apply(boundThis, [...boundArgs, ...callArgs]);
};
}
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const bound = myBind(greet, { name: "Sam" }, "Hey");
bound("!"); // "Hey, Sam!"The real Function.prototype.bind also has a subtlety this version skips: a bind-created function used as a constructor (new boundFn()) ignores the bound this and uses the new instance instead, new binding beats even explicit binding. Try extending myBind to handle that case using new.target or a prototype check.
In React
Class components made this topic unavoidable, event handler methods lose their this when passed as a bare reference (onClick={this.handleClick} is a default-binding call site, identical to the fn() example above), which is why you'd see constructors full of this.handleClick = this.handleClick.bind(this), or handlers written as class fields with arrow functions (handleClick = () => {...}) specifically to lock in this lexically.
Function components sidestep almost all of this, hooks don't use this at all, and event handlers are usually defined as regular const functions or arrow functions inside the component, closing over the values they need directly rather than reading them off this. If you still see a this-related bug in modern React, it's almost always inside a class component, or a plain (non-React) callback like the setInterval example above.
Common Mistakes
1. Destructuring a method off an object
const { regular } = obj; // detaches the method from `obj`
regular(); // undefined, default binding, `obj.` context is goneDestructuring a method is exactly equivalent to const fn = obj.method, it silently strips the implicit binding. Either keep the method attached (obj.regular()) or .bind(obj) it first.
2. Passing a method as a callback
class Logger {
prefix = "[LOG]";
log(msg) { console.log(this.prefix, msg); }
}
const logger = new Logger();
[1, 2, 3].forEach(logger.log); // TypeError-ish: this.prefix is undefinedforEach calls the passed function as a bare function, default binding, completely unaware it was ever logger.log. Fix: .forEach((msg) => logger.log(msg)), or .bind(logger), or make log an arrow class field.
3. Using an arrow function for an object method that needs dynamic this
const counter = {
count: 0,
increment: () => { this.count++; }, // ❌ arrow, `this` is NOT `counter`
};
counter.increment(); // this.count silently refers to outer scope's `this`, not counterArrow functions are wrong for object methods precisely because they don't participate in implicit binding at all, this inside increment is whatever this was in the surrounding scope where the object literal was written (often the module scope, where this is undefined).
Best Practices
- Use regular functions for object methods, so implicit binding works as expected.
- Use arrow functions for callbacks that should inherit
thisfrom their surrounding context (timers, event listeners, array methods, promise callbacks), this is the primary reason arrow functions exist. - Prefer
bindover manually savingconst self = this(an old pre-arrow-function pattern) when you need to permanently lock a function'sthis. - Never mix
newwith a function designed as a plain callback, and vice versa, a function's calling convention should be unambiguous from how it's written and named.
Performance Tips
.bind()allocates a new function object every time it's called, creating a bound function inside a render function or a hot loop (onClick={this.handleClick.bind(this)}in JSX, evaluated on every render) creates unnecessary garbage. Bind once (in a constructor, or as a class field) rather than on every call/render..call()and.apply()are effectively free, the only difference between them and a normal call is how arguments are passed, with negligible overhead either way.
