-
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)
201: Practice Exercise: Implementing a Simple Event Emitter Class
You've likely spent plenty of time using addEventListener in the browser, but as you move into more complex application architecture, you'll find that you often need this "pub-sub" (publisher-subscriber) behavior in places where the DOM doesn't exist—like in a backend service or a state management layer. Implementing your own Event Emitter is a rite of passage in JavaScript because it forces you to think about how data flows between decoupled parts of your system.
The "Single Slot" Mistake
When I first started building these, I took the path of least resistance. I'd create a class and just add a property for the event I cared about. If I were building a User Auth system, I might do something like this:
class AuthEmitter {
constructor() {
this.onLogin = null;
}
on(callback) {
this.onLogin = callback;
}
emit() {
if (this.onLogin) this.onLogin();
}
}
On the surface, this works. But it's fragile. The moment you realize that three different parts of your app need to know when a user logs in—say, the Header component to show the profile picture, the Analytics module to track the session, and the Dashboard to fetch user data—this code breaks. You've created a "single slot" system. Every time you call .on(), you're overwriting the previous listener. In a real production environment, this leads to those phantom bugs where some features just stop working because another developer added a listener elsewhere in the codebase.
Moving to a Registry of Arrays
To fix this, we have to stop thinking about a single callback and start thinking about a registry. Instead of a property, we use an object (or a Map) where the keys are the event names and the values are arrays of functions. This allows us to support any number of events, each with any number of listeners.
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
const listeners = this.events[eventName];
if (listeners) {
listeners.forEach(callback => callback(data));
}
}
}
Now we have a flexible system. I can call emitter.on('userLogin', handleUI) and emitter.on('userLogin', handleLogs) without them fighting each other. I've also added a data argument to the emit method, which is essential. An event that just says "something happened" is rarely useful; you usually need to know what happened or who it happened to.
The Memory Leak Problem
Here is where most developers stop, but this is where the "professional" version differs from the "tutorial" version. If you keep adding listeners to a long-lived object but never remove them, you're creating memory leaks. In a single-page app, if a component mounts, adds a listener, and then unmounts without removing that listener, that component stays in memory forever because the EventEmitter still holds a reference to its method.
To solve this, we need an off method. The trick here is that you can't just clear the array; you have to remove the specific function reference. I usually use filter for this because it's clean and immutable, though splice is slightly more performant if you're dealing with thousands of listeners (which, let's be honest, you probably aren't).
off(eventName, callbackToRemove) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(
callback => callback !== callbackToRemove
);
}
Keep in mind: for off to work, you can't use anonymous arrow functions in your .on() call. If you pass () => console.log('hi'), you're creating a new function reference every time, and filter will never find a match to remove. You have to name your functions if you want to be able to unsubscribe from them later.
📋 Practical Task
Exercise: Build a Shopping Cart Notification Hub
Your goal is to implement a fully functional CartEmitter class that manages events for an e-commerce shopping cart. This hub will allow different parts of the application (the Cart Icon, the Total Price calculator, and the Inventory system) to stay in sync without being directly connected.
Requirements:
- Implement an
on(event, callback)method to register listeners. - Implement an
emit(event, data)method that triggers all registered callbacks for that event, passing the data along. - Implement an
off(event, callback)method to remove a specific listener.
Test your implementation with the following scenario:
- Create an instance of
CartEmitter. - Create two separate functions:
updateCartCountandlogTransaction. - Subscribe both functions to the
'itemAdded'event. - Emit the
'itemAdded'event with a product object (e.g.,{ name: 'Mechanical Keyboard', price: 150 }) and verify both functions execute. - Unsubscribe
logTransactionusing theoffmethod. - Emit
'itemAdded'again and verify that onlyupdateCartCountexecutes.
There are no comments for now.