JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
33: call, apply, and bind
One of the most frustrating things about JavaScript is how this behaves. You’ve probably already noticed that its value isn't determined by where a function is defined, but by how it's called. This leads to a common problem: you have a piece of logic that needs to work on several different objects, but you don't want to redefine that logic inside every single object literal.
The redundancy trap
Imagine you're building a system to handle different types of entities—users, companies, and departments—all of which have a name and a location. You want a way to print a formatted description for any of them. The naive approach is to just give every object its own method:
const user = {
name: 'Alice',
city: 'New York',
describe() {
return `${this.name} is based in ${this.city}`;
}
};
const company = {
name: 'Acme Corp',
city: 'Chicago',
describe() {
return `${this.name} is based in ${this.city}`;
}
};
This works, but it's a maintenance disaster. If you decide to change the wording to "located in" instead of "based in," you're hunting through every object in your codebase to update a string. You might think, "I'll just move the function outside and pass the object as an argument," which is a perfectly valid functional approach. But sometimes, you're working with an API or a library that expects a method to be called on an object, or you simply want the semantic clarity of "borrowing" a method.
Swapping contexts in real-time
This is where call and apply come in. They allow you to execute a function and explicitly tell JavaScript: "I want this to be this specific object, regardless of where the function actually lives."
I usually define a standalone function and then "borrow" it. Look at how much cleaner this is:
const user = { name: 'Alice', city: 'New York' };
const company = { name: 'Acme Corp', city: 'Chicago' };
function describe() {
return `${this.name} is based in ${this.city}`;
}
// Use .call() to specify the context
console.log(describe.call(user)); // "Alice is based in New York"
console.log(describe.call(company)); // "Acme Corp is based in Chicago"
You'll notice call takes the context as the first argument, followed by any arguments the function needs, passed individually. apply is almost identical, but it takes arguments as an array. I rarely use apply these days because the spread operator (...) has made it mostly redundant, but it's still vital to know. If you have an array of data and you want to pass it into a function that expects individual arguments, apply is your tool.
Preserving 'this' for the long haul
The problem with call and apply is that they execute the function immediately. But what happens when you need to pass a method as a callback—like to a setTimeout or a click handler—and you want it to remember which object it belongs to?
If you pass a method directly, you'll find that this becomes undefined (in strict mode) or the global window object. It's a classic JS gotcha. bind solves this by creating a new function that is permanently locked to a specific context.
const user = {
name: 'Alice',
greet() {
console.log(`Hi, I'm ${this.name}`);
}
};
// This will fail because 'this' is lost when the timer fires
setTimeout(user.greet, 1000); // "Hi, I'm undefined"
// This works because bind creates a bound copy of the function
const boundGreet = user.greet.bind(user);
setTimeout(boundGreet, 1000); // "Hi, I'm Alice"
I like to think of bind as "pre-configuring" a function. You aren't running the code yet; you're just preparing it to be run later with the correct identity attached. The trade-off is a tiny bit of memory overhead because you're creating a new function instance, but the safety it provides in asynchronous code is non-negotiable.
📋 Practical Task
Exercise: The Shared Logger Implementation
You are building a logging system where different modules (Auth, Database, and Network) need to log messages. Instead of writing a log method for every module, you've created a single shared logger function.
Your Task:
- Create a function called
logMessagethat takes two arguments:levelandmessage. It should return a string in the format:"[LEVEL] [ModuleName]: message". Usethis.moduleNameto get the name of the module. - Create two objects:
authModuleanddbModule. Both should have a propertymoduleName(e.g., "Authentication" and "Database"). - Use
.call()` to log an "INFO" message for theauthModule. - Use
.apply()` to log an "ERROR" message for thedbModule, passing the arguments as an array. - Create a bound version of the
logMessagefunction specifically for theauthModulecalledauthLogger, and use it to log a "WARN" message.
There are no comments for now.