The Singleton Pattern in 30 Lines of Vanilla JS: One Popup Manager to Rule Them All
Managing Popups with the Singleton Pattern: Understanding Singleton from a Piece of Vanilla JS
Summary
When multiple places on a page can open popups, the worst thing is to new an unrelated object each time—you can't "record all popups and close them all at once." This article uses a 30-line vanilla JS popup code snippet to break down the three-step template of the Singleton pattern, lazy loading, and the distinction between "static methods / instance methods," which is the easiest point for beginners to confuse. After reading, you will be able to write a globally unique Popup manager yourself.
Opening: A Popup That "Can't Be Managed"
You are building a webpage with an "Open New Page" button. Clicking it uses window.open to open a new tab. The functionality is simple, but when there are many popups, a problem arises: if each opening logic news its own object, they are unaware of each other, and unified management becomes impossible.
I also got stuck here when I first started learning design patterns. Singleton is one of the most fundamental patterns in object-oriented programming and one of the most commonly used in enterprise-level projects—it ensures that a class is instantiated only once in the system. Understanding it is not just about writing one less new; it's about understanding the mindset of "sharing the same state globally." This article will first run through this popup code to see the effect, then break down the three-step template of Singleton, then clarify lazy loading and the difference between "static methods / instance methods," and finally connect it to a DOM button. Reading this article only requires knowledge of JS class and basic DOM events.
Run It First: Two getInstance Calls Return the Same Object
const a = Popup.getInstance();
const b = Popup.getInstance();
console.log(a === b); // true
a and b are the same reference, so a === b is true. This is precisely the meaning of Singleton—there is only this one Popup object globally, and getting it anywhere means operating on the same data. For example, if you record a popup list in the instance, other places can read it and "close all at once," because they are fundamentally the same object.
Singleton Template in Three Steps: Store Instance → Check Existence → Unified Entry
class Popup {
static ins; // Singleton instance, static property
static getInstance() {
if (!Popup.ins) {
Popup.ins = new Popup();
}
return Popup.ins;
}
// ...
}
The Singleton template can be broken down into three steps:
- Store Instance: Use the static property
static insto hang the unique instance on the class, rather than storing it in an external variable. - Check Existence:
if (!Popup.ins)checks whether the instance exists. - Unified Entry: Always get the object through
getInstance(), never directlynew.
The control flow inside getInstance() looks like this—the first time it's called, ins is empty, so it news one and saves it; subsequent calls find it already exists and return it directly:
flowchart TD
A[Call getInstance] --> B{Instance exists?}
B -->|Does not exist| C[new creates instance]
B -->|Exists| D[Return directly]
C --> E[Return same instance]
D --> E
Lazy Loading: Only new on First Call, No Manual new Needed
Note the line if (!Popup.ins) { Popup.ins = new Popup(); }: the instance is not created at class definition time, but is newed only when getInstance() is called for the first time, and then reused thereafter. This is called lazy initialization—create only when needed, avoiding resource occupation from the start. So you never need to manually new Popup(); the first call to getInstance() will automatically create it for you.
Static Methods vs. Instance Methods: Why getInstance Can Be Called Directly with "ClassName."
const a = Popup.getInstance(); // Static method, directly Popup.xxx
a.open("https://www.baidu.com"); // Instance method, must first get instance a
The distinction here is the easiest for beginners to confuse: getInstance has static in front, making it a static method attached to the class, so it can be called directly as Popup.getInstance() without new; whereas open() does not have static, making it an instance method, which requires a specific instance a to call a.open(). Remember it in one sentence—static methods are called with "ClassName.", instance methods are called with "instance.".
Connecting to the DOM: getElementById and Event Binding
const openBtn = document.getElementById("openBtn");
openBtn.addEventListener("click", () => {
a.open("https://www.baidu.com");
});
getElementById("openBtn") finds the <button id="openBtn"> element on the page by id; addEventListener binds a click event to it; clicking calls a.open(). Note that the Singleton a is used here, so no matter where on the page it's triggered, it goes through the same Popup instance.
The window.open(url, '_blank') Line Inside open
open(url) {
window.open(url, '_blank');
}
window.open is used to open a new window/tab; the second parameter '_blank' means open in a new tab (rather than replacing the current page, or opening in a specifically named frame). Here, "which url to open" is passed in as a parameter, decided by the external caller.
Why Use Singleton to Manage Popups Instead of new Each Time
In real projects, popups often need to "record all, close uniformly." Singleton allows all opening logic to share the same instance, with the popup list naturally centralized in one place. The first time I wrote this kind of functionality, I took a shortcut and directly new Popup() twice, resulting in two buttons each holding different instances, and the close logic couldn't find each other—that's the pitfall of not using Singleton. After switching to Singleton, any entry point gets the same object, and centralized management immediately works.
Summary
| Concept | One-Sentence Explanation | Key Code |
|---|---|---|
| Singleton | Instantiated only once globally, shared everywhere | static ins + getInstance() |
| Lazy Loading | Only new on first call |
if (!Popup.ins) Popup.ins = new Popup() |
| Static Method | Attached to class, called directly with class name | static getInstance() |
| Instance Method | Requires an instance first to call | a.open(url) |
window.open _blank |
Opens in a new tab | window.open(url, '_blank') |
| getElementById | Gets DOM element by id | document.getElementById('openBtn') |
Common Mistakes and Further Learning
- Common Mistake: Writing
openasstatictoo will prevent calling state on the instance; confusingstaticwith instance methods is a high-frequency beginner error. - Real Project Note: A globally unique Singleton is not easily replaceable in unit tests (coupling global state). Large projects often use dependency injection or module-level singletons to mitigate this.
- To Supplement: Implementing Singleton with closures / module patterns, writing it in TypeScript, and "when you actually shouldn't use Singleton" (multi-instance scenarios).
Conclusion
After learning this, Singleton is no longer an abstract term—it is simply "locking a unique instance with a static property + a unified entry to get it." In scenarios like popup management that require globally shared state, it is much cleaner than newing everywhere. The next step is to look at module-level singletons and TypeScript implementations, applying this mindset to larger projects.
- Can articulate the Singleton "three-step template" and write a minimal implementation
- Can explain why
a === bistrue - Can distinguish the calling methods of
staticmethods and instance methods - Can explain why lazy loading saves resources
- Can state the meaning of the second parameter
_blankinwindow.open - Can give a real scenario where "using Singleton is more appropriate than
neweach time"