深入理解JavaScript单例模式:从Storage封装到Modal弹窗的实战应用

前言

在日常开发中,我们经常遇到这样的场景:需要一个全局唯一的对象来管理某些资源,比如数据库连接池、缓存管理器,或者页面上的弹窗组件。这时候,单例模式就显得尤为重要了。

单例模式作为23种设计模式中的一种,它的核心思想很简单:确保一个类只有一个实例,并提供全局访问点。今天我们就来深入探讨一下在JavaScript中如何优雅地实现单例模式。

什么是单例模式?

单例模式是一种创建型设计模式,它保证一个类仅有一个实例,并提供一个访问它的全局访问点。这种模式在需要控制资源访问、避免重复创建对象的场景中非常有用。

单例模式的核心特征:

  • 类只能有一个实例
  • 必须自行创建这个实例
  • 必须给其他对象提供这一实例

ES6 Class实现单例模式

让我们从一个实际的例子开始------实现一个基于localStorage的Storage类。

基础实现

js 复制代码
class Storage {
    static instance;
    
    constructor() {
        console.log(this, '~~~');
    }
    
    // 静态方法:获取单例实例
    static getInstance() {
        // 如果还没有实例化过,则创建新实例
        if (!Storage.instance) {
            Storage.instance = new Storage();
        }
        return Storage.instance;
    }
    
    getItem(key) {
        return localStorage.getItem(key);
    }
    
    setItem(key, value) {
        localStorage.setItem(key, value);
    }
}

// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();

console.log(storage1 === storage2); // true - 证明是同一个实例

storage1.setItem('name', '鹿老板');
console.log(storage1.getItem('name')); // 鹿老板
console.log(storage2.getItem('name')); // 鹿老板 - 同一个实例,数据共享

关键点解析

  1. static instance:静态属性,用于保存唯一的实例
  2. static getInstance() :静态方法,负责创建和返回实例
  3. 惰性实例化:只有在第一次调用时才创建实例,提高性能

这种实现方式的优势在于:

  • 语法简洁,易于理解
  • 利用ES6的static关键字,代码更加规范
  • 性能优秀,避免了重复创建对象的开销

闭包实现单例模式

在ES6之前,我们通常使用闭包来实现单例模式。这种方式虽然看起来复杂一些,但理解了闭包的原理后,会发现它的设计非常巧妙。

js 复制代码
// 基础构造函数
function StorageBase() {
    // 构造函数体
}

StorageBase.prototype.getItem = function(key) {
    return localStorage.getItem(key);
};

StorageBase.prototype.setItem = function(key, value) {
    localStorage.setItem(key, value);
};

// 使用闭包实现单例
const Storage = (function() {
    let instance = null; // 自由变量,保存唯一实例
    
    return function() {
        if (!instance) {
            instance = new StorageBase();
        }
        return instance;
    };
})();

// 使用方式
const storage1 = new Storage();
const storage2 = new Storage();

console.log(storage1 === storage2); // true

storage1.setItem('name', 'V老板');
console.log(storage1.getItem('name')); // V老板
console.log(storage2.getItem('name')); // V老板

闭包实现的优势

  1. 作用域隔离:instance变量被封闭在闭包内,外部无法直接访问
  2. 兼容性好:不依赖ES6语法,在老版本浏览器中也能正常工作
  3. 灵活性强:可以在闭包内部添加更多的私有变量和方法

实战应用:Modal弹窗单例

理论讲完了,让我们来看一个更贴近实际开发的例子------登录弹窗的单例实现。

业务场景分析

在实际项目中,登录弹窗有以下特点:

  • 全站只需要一个登录弹窗
  • 90%的用户可能不会登录,不应该在页面加载时就创建DOM
  • 需要支持多次打开/关闭,但始终是同一个DOM元素

代码实现

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Modal 登入弹窗单例</title>
    <style>
        #modal {
            position: fixed;
            line-height: 200px;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 200px;
            height: 200px;
            border: 1px solid #000;
            text-align: center;
            background: white;
            z-index: 1000;
        }
    </style>
</head>
<body>
    <button id="open">打开弹窗</button>
    <button id="close">关闭弹窗</button>
    <button id="open2">打开弹窗2</button>
    
    <script>
        // 使用闭包实现Modal单例
        const Modal = (function() {
            let modal = null;
            
            return function() {
                if (!modal) {  // 第一次和唯一一次创建
                    modal = document.createElement('div');
                    modal.innerHTML = '我是一个全局唯一的Modal';
                    modal.id = 'modal';
                    modal.style.display = 'none';
                    document.body.appendChild(modal);
                }
                return modal;
            };
        })();
        
        // 事件绑定
        document.getElementById('open').addEventListener('click', function() {
            const modal = new Modal();
            modal.style.display = 'block';
        });
        
        document.getElementById('close').addEventListener('click', function() {
            const modal = new Modal();
            modal.style.display = 'none';
        });
        
        document.getElementById('open2').addEventListener('click', function() {
            const modal = new Modal();
            modal.style.display = 'block';
        });
    </script>
</body>
</html>

实现亮点

  1. 懒加载:DOM元素在第一次需要时才创建,避免了不必要的性能开销
  2. 资源复用:多个按钮操作的都是同一个DOM元素,节省内存
  3. 状态管理:通过display属性控制显示/隐藏,状态在实例间共享

单例模式的优缺点

优点

  1. 内存节省:确保只有一个实例存在,减少内存占用
  2. 全局访问:提供全局访问点,方便状态管理
  3. 延迟实例化:支持懒加载,按需创建实例
  4. 线程安全:在JavaScript单线程环境中,天然避免了多线程同步问题

缺点

  1. 测试困难:全局状态使得单元测试变得复杂
  2. 扩展性差:违反了开闭原则,不易扩展
  3. 隐式依赖:代码之间的依赖关系不够明确

适用场景

  • 配置管理器
  • 日志记录器
  • 数据库连接池
  • 缓存管理器
  • 弹窗、Toast等UI组件

现代JavaScript中的替代方案

模块模式

js 复制代码
// storage.js
class StorageManager {
    constructor() {
        this.cache = new Map();
    }
    
    getItem(key) {
        return localStorage.getItem(key);
    }
    
    setItem(key, value) {
        localStorage.setItem(key, value);
    }
}

// 导出单例实例
export default new StorageManager();

使用Symbol确保唯一性

js 复制代码
const INSTANCE = Symbol('instance');

class Storage {
    static [INSTANCE] = null;
    
    static getInstance() {
        if (!Storage[INSTANCE]) {
            Storage[INSTANCE] = new Storage();
        }
        return Storage[INSTANCE];
    }
}

总结

单例模式作为一种经典的设计模式,在JavaScript开发中有着广泛的应用。通过本文的学习,我们了解了:

  1. ES6 Class实现:语法简洁,易于理解和维护
  2. 闭包实现:兼容性好,作用域隔离更彻底
  3. 实战应用:Modal弹窗展示了单例模式在UI组件中的价值
  4. 现代替代方案:模块模式和Symbol的使用

在实际开发中,我们应该根据具体场景选择合适的实现方式,既要考虑代码的可维护性,也要关注性能和用户体验。设计模式是工具,而不是目的,合理使用才能发挥其真正的价值。

相关推荐
EnCi Zheng17 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen21 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技21 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人33 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实33 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha44 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
不可能的是1 小时前
从 /simplify 指令深挖 Claude Code 多 Agent 协同机制
javascript