Course: JavaScript Advanced
Lesson 4: The this Keyword
Level
Advanced
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Explain how
thisis determined in different calling contexts - Use
call,apply, andbindto explicitly setthis - Explain why arrow functions do not have their own
thisand when to use them
Frontend Development Context
The this keyword is the source of more JavaScript confusion than almost any other feature. In event listeners, class methods, callbacks, and object methods, this can silently point to the wrong object, causing undefined property errors that are frustrating to debug. Every frontend developer has encountered code where this unexpectedly became the global object or undefined inside a callback.
Understanding how this is determined — and how to control it with call, apply, bind, and arrow functions — is essential for writing reliable class-based components, event handlers, and reusable utility functions. It also explains why React class components require .bind(this) in constructors, why Vue and React hooks favor arrow functions, and how this works inside modern framework internals.
Explanation
In JavaScript, this is not fixed at the time a function is written — it is determined dynamically at the time the function is called, based on how it is called. There are four main rules that determine this.
Default binding: If a regular function is called standalone (not as a method, not with new, not with explicit binding), this is the global object (window in browsers) in non-strict mode, or undefined in strict mode. This is the implicit fallback and often the source of unintended behavior.
Implicit binding: If a function is called as a method of an object — obj.method() — then this inside method refers to obj. However, this binding is fragile: if you extract the method and call it separately (const fn = obj.method; fn()), the implicit binding is lost and you fall back to default binding.
Explicit binding: Function.prototype.call, apply, and bind let you set this explicitly. call(thisArg, arg1, arg2) invokes the function immediately with the given this. apply(thisArg, [args]) does the same but accepts arguments as an array. bind(thisArg) returns a new function permanently bound to thisArg, regardless of how it is later called.
Arrow functions: Arrow functions do not have their own this at all. They capture this from the surrounding lexical scope at the time they are defined, not when they are called. This makes them ideal for callbacks inside methods, where you want this to refer to the outer object rather than the callback's own calling context.
new binding: When a function is called with new, a new object is created, this is set to that new object, and the function's prototype is linked. This is how constructor functions and classes work under the hood.
Code Example
This example demonstrates implicit binding, lost binding, explicit binding with bind, and arrow functions capturing lexical this.
const timer = {
label: "Dashboard Timer",
seconds: 0,
// Regular method — this works fine when called as timer.start()
start() {
console.log(`Starting: ${this.label}`);
// Arrow function captures `this` from start()'s scope = timer
const tick = () => {
this.seconds++;
console.log(`${this.label}: ${this.seconds}s`);
};
// Simulate 3 ticks
tick(); tick(); tick();
},
// Demonstrates lost binding
getLabel() {
return this.label;
},
};
timer.start(); // "Starting: Dashboard Timer", then 3 ticks
// Implicit binding — works
console.log(timer.getLabel()); // "Dashboard Timer"
// Lost binding — this becomes undefined (strict) or window
const detached = timer.getLabel;
try {
console.log(detached()); // TypeError in strict mode
} catch (e) {
console.log("Lost binding:", e.message);
}
// Explicit binding with bind — permanent fix
const boundGetLabel = timer.getLabel.bind(timer);
console.log(boundGetLabel()); // "Dashboard Timer"
// call — invoke immediately with a different this
const altContext = { label: "Sidebar Timer" };
console.log(timer.getLabel.call(altContext)); // "Sidebar Timer"
// apply — same as call but args as array
function formatTimer(prefix, suffix) {
return `${prefix} ${this.label} ${suffix}`;
}
console.log(formatTimer.apply(altContext, [">>", "<<"])); // ">> Sidebar Timer <<"
Code Explanation
Inside timer.start(), this is timer because of implicit binding — start is called as a method of timer. The inner tick function is an arrow function, so it doesn't get its own this; it inherits this from start's scope, which is timer. Each tick() call correctly increments timer.seconds and reads timer.label.
The getLabel method works correctly when called as timer.getLabel() — implicit binding makes this refer to timer. But when extracted to detached and called as a standalone function, the implicit binding is lost. In strict mode this throws a TypeError because this is undefined and undefined.label fails.
bind(timer) creates a new function where this is permanently locked to timer. No matter how boundGetLabel is later called — as a callback, assigned to another variable, passed to an event listener — this will always be timer.
call and apply demonstrate immediate invocation with a custom this. call is used with individual arguments; apply is used when the arguments are already in an array (useful for variadic functions or forwarding arguments).
Pattern Highlights
✅ Positive Pattern: Use arrow functions for callbacks inside object methods to capture the outer
thislexically — this avoids the need forconst self = thisworkarounds and keeps code clean.
⚠️ Neutral Note:
bindcreates a new function each time it is called. In React class components, calling.bind(this)insiderender()creates a new function on every render — prefer binding in the constructor or using arrow function class properties instead.
❌ Negative Pattern: Do not use arrow functions as object methods when you need
thisto refer to the object — arrow functions capturethisfrom where they are defined (likely the global scope or enclosing function), not from the object they are assigned to.
Common Mistakes
Mistake 1: Using an arrow function as an object method
Arrow functions don't have their own this, so using one as a method means this will not be the object.
const button = {
label: "Submit",
handleClick: () => {
console.log(this.label); // undefined — this is window or undefined
},
};
button.handleClick(); // not what you expect
Use a regular function expression or method shorthand (handleClick() {}) so this is bound to button when called as a method.
Mistake 2: Passing a method as a callback without binding
Passing obj.method as a callback loses the implicit binding.
class Carousel {
constructor() {
this.index = 0;
document.querySelector(".next")
.addEventListener("click", this.advance); // this is lost!
}
advance() {
this.index++; // TypeError: Cannot set properties of undefined
}
}
Fix: use this.advance.bind(this) or an arrow function wrapper () => this.advance() in the addEventListener call.
Mistake 3: Relying on this inside a setTimeout callback
Regular functions in setTimeout use default binding, so this is not the surrounding object.
const notification = {
message: "Done!",
show() {
setTimeout(function () {
console.log(this.message); // undefined — this is window
}, 1000);
},
};
Replace the regular function with an arrow function: setTimeout(() => { console.log(this.message); }, 1000). The arrow function captures this from show, which is correctly bound to notification.
Quick Recap
thisin JavaScript is determined by how a function is called, not where it is defined — there are four binding rules: default, implicit, explicit (call/apply/bind), andnew.- Implicit binding is lost when a method is extracted from its object and called as a standalone function.
- Arrow functions do not have their own
this— they lexically capturethisfrom their surrounding scope at definition time, making them ideal for nested callbacks inside methods. - Use
bindto permanently lockthisfor callbacks like event listeners; usecallorapplyto temporarily invoke a function with a specificthis.