深入理解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的使用

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

相关推荐
GoldKey2 小时前
gcc 源码阅读---语法树
linux·前端·windows
Xf3n1an3 小时前
html语法
前端·html
张拭心3 小时前
亚马逊 AI IDE Kiro “狙击”Cursor?实测心得
前端·ai编程
漠月瑾-西安3 小时前
如何在 React + TypeScript 中实现 JSON 格式化功能
javascript·jst实现json格式化
烛阴3 小时前
为什么你的Python项目总是混乱?层级包构建全解析
前端·python
止观止3 小时前
React响应式组件范式:从类组件到Hooks
javascript·react.js·ecmascript
@大迁世界3 小时前
React 及其生态新闻 — 2025年6月
前端·javascript·react.js·前端框架·ecmascript
LJianK14 小时前
Java和JavaScript的&&和||
java·javascript·python
红尘散仙4 小时前
Rust 终端 UI 开发新玩法:用 Ratatui Kit 轻松打造高颜值 CLI
前端·后端·rust
新酱爱学习4 小时前
前端海报生成的几种方式:从 Canvas 到 Skyline
前端·javascript·微信小程序