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)
157: The Singleton Pattern
At some point in your project, you're going to realize you have a piece of state that absolutely must be the same everywhere in your application. I usually see this happen with things like a database connection pool, a global configuration object, or a user session manager. The last thing you want is three different parts of your app holding three different versions of your "global" settings because you accidentally instantiated the manager class three times.
The chaos of multiple instances
The naive way to handle this is to just create a class and instantiate it wherever you need it. It feels natural—that's how we're taught OOP—but it creates a synchronization nightmare. Imagine we're building a ConfigurationManager to handle API keys and theme settings.
class ConfigurationManager {
constructor() {
this.settings = { theme: 'light', apiVersion: 'v1' };
}
setSetting(key, value) {
this.settings[key] = value;
}
getSetting(key) {
return this.settings[key];
}
}
// In file A.js
const configA = new ConfigurationManager();
configA.setSetting('theme', 'dark');
// In file B.js
const configB = new ConfigurationManager();
console.log(configB.getSetting('theme')); // 'light' - Wait, what?
The problem here is that configA and configB are entirely different objects in memory. You've essentially created two separate sources of truth. If you're trying to change the theme of your entire app, you'll find yourself wondering why half your components are still in light mode. You could try to pass the instance around as a prop or argument through ten different layers of your app, but that's just "prop drilling" and it makes your code brittle and tedious to maintain.
The classic Singleton approach
To fix this, we want to ensure that no matter how many times we *try* to create a new instance, we always get back the exact same one. The traditional way to do this is to hijack the constructor. I'll be honest: this is the way you'll see it in Java or C# textbooks, and while it works in JS, it feels a bit clunky.
class ConfigurationManager {
constructor() {
if (ConfigurationManager.instance) {
return ConfigurationManager.instance;
}
this.settings = { theme: 'light', apiVersion: 'v1' };
ConfigurationManager.instance = this;
}
setSetting(key, value) {
this.settings[key] = value;
}
getSetting(key) {
return this.settings[key];
}
}
const configA = new ConfigurationManager();
const configB = new ConfigurationManager();
console.log(configA === configB); // true
By checking for a static instance property, the constructor effectively says, "If I've already been built once, just give them the old one." It solves the problem, but it's a bit deceptive. When you call new, you expect a new object, not a cached one. It's a "magic" behavior that can confuse other developers on your team.
The idiomatic JavaScript way
In modern JavaScript, we have a much cleaner tool for this: the module system. ES modules are cached the first time they are imported. This means if you instantiate your class inside the module and export that specific instance, Node.js or the browser will ensure that every other file importing that module gets the same instance. It's simpler, more explicit, and removes the need for weird constructor logic.
// configManager.js
class ConfigurationManager {
constructor() {
this.settings = { theme: 'light', apiVersion: 'v1' };
}
setSetting(key, value) {
this.settings[key] = value;
}
getSetting(key) {
return this.settings[key];
}
}
// We instantiate it here, once.
const instance = new ConfigurationManager();
export default instance;
Now, in any other file, you just import config from './configManager.js'. You aren't calling new anymore; you're just grabbing the existing instance. I prefer this because it treats the singleton as a shared resource rather than a trick of the constructor. You get the guarantee of a single source of truth without the "magic" that makes debugging a headache.
📋 Practical Task
Build a Shared AppState Singleton
You are building a small application that needs to track a global AppState (containing the current currentPage and isLoggedIn status) across multiple files.
Your task is to implement this using the idiomatic JavaScript module pattern. Create a AppState class with methods to updateState(key, value) and getState(key). Ensure that when you export the state, any file importing it interacts with the exact same instance. To verify your work, simulate two different modules importing the state: have the first module update the currentPage to "Dashboard", and have the second module log that value to the console to prove they are sharing the same object.
There are no comments for now.