单例模式在前端开发中其实很常见?手把手教你封装一个LocalStorage单例

"如何设计一个全局唯一的本地存储工具类?" 这个问题看似简单,实则暗藏玄机 ------ 它考察的是单例模式(Singleton Pattern)的应用。单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。

为什么需要单例模式?从存储工具说起

在前端开发中,我们经常需要操作本地存储(LocalStorage)。但如果在多个地方分别创建存储实例,可能会导致以下问题:

  1. 资源浪费:每次创建实例都会占用额外的内存空间;
  2. 数据不一致:不同实例操作同一份数据时,可能因为状态不同步导致数据冲突;
  3. 难以维护:多个实例分散在代码中,增加了维护成本。

而单例模式可以完美解决这些问题:确保一个类只有一个实例,并提供全局访问点。比如,一个应用中应该只有一个 LocalStorage 的操作实例,所有地方都通过这个实例来读写数据。

单例模式的核心:唯一实例与全局访问

单例模式的实现有两个关键点:

  1. 唯一实例:类的构造函数需要被限制,不能随意创建新实例;
  2. 全局访问:提供一个静态方法或属性,让外界可以获取这个唯一实例。
javascript 复制代码
function Storage() {
  this.data = {};

  // 实例方法:存储数据
  this.setItem = function(key, value) {
    this.data[key] = value;
  };

  // 实例方法:获取数据
  this.getItem = function(key) {
    return this.data[key];
  };
}

// 通过new创建两个实例
const storage1 = new Storage();
const storage2 = new Storage();

console.log(storage1 === storage2, '~~~');

在 JavaScript 中,实现单例模式有多种方式,下面我们通过实现一个单例的Storage类来逐一讲解。

(1)ES6 Class 实现:静态属性 + 静态方法

使用 ES6 的class语法,结合静态属性和静态方法,可以简洁地实现单例模式:

javascript 复制代码
class Storage {
  // 静态属性:存储唯一实例
  static instance = null;

  // 私有构造函数(可选)
  constructor() {
    // 如果已存在实例,直接返回,阻止新实例创建
    if (Storage.instance) {
      return Storage.instance;
    }
    // 初始化逻辑
    console.log('Storage实例初始化');
    Storage.instance = this; // 将当前实例赋值给静态属性
  }

  // 静态方法:获取唯一实例
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }

  // 存储方法
  setItem(key, value) {
    localStorage.setItem(key, value);
  }

  getItem(key) {
    return localStorage.getItem(key);
  }
}

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

console.log(storage1 === storage2); 

storage1.setItem('name', '张三');
console.log(storage2.getItem('name')); 

关键点解析

  • static instance:类的静态属性,用于存储唯一实例;
  • static getInstance():静态方法,负责创建或返回已存在的实例;
  • 构造函数中判断instance是否存在,避免重复创建实例。

(2)闭包实现:立即执行函数 + 自由变量

除了类的方式,还可以使用闭包(立即执行函数)来实现单例:

javascript 复制代码
// 基类:定义存储方法
class StorageBase {
  setItem(key, value) {
    localStorage.setItem(key, value);
  }

  getItem(key) {
    return localStorage.getItem(key);
  }
}

// 单例包装
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); 

storage1.setItem('age', '25');
console.log(storage2.getItem('age')); 

闭包实现的本质

  • 立即执行函数创建了一个闭包环境;
  • instance变量被闭包捕获,始终保持在内存中;
  • 每次调用Storage()时,返回的都是同一个instance

(3)单例模式的 "懒汉式" 与 "饿汉式"

根据实例创建的时机,单例模式可分为两种:

  1. 懒汉式(Lazy Initialization)

    • 实例在第一次被使用时创建(如前面的例子);
    • 优点:延迟加载,节省资源;
    • 缺点:多线程环境下可能存在线程安全问题(JS 单线程,无需担心)。
  2. 饿汉式(Eager Initialization)

    • 实例在类加载时就创建;
    • 优点:线程安全,无需判断;
    • 缺点:提前占用资源。

下面是饿汉式的实现示例:

javascript 复制代码
class Storage {
  // 类加载时就创建实例
  static instance = new Storage();

  private constructor() {} // 私有构造函数

  static getInstance() {
    return Storage.instance;
  }

  // 存储方法...
}

实战:单例模式实现全局唯一登录弹窗

单例模式的应用远不止存储工具,登录弹窗也是典型场景 ------ 无论从哪个入口(导航栏、按钮、评论区)触发登录,都应该显示同一个弹窗实例。

需求分析

  • 页面中多个按钮可打开登录弹窗;
  • 弹窗全局唯一,避免重复创建 DOM;
  • 支持显示 / 隐藏操作,状态统一。

完整实现代码

html 复制代码
<body>
    <button id="open">打开弹窗</button>
    <button id="close">关闭弹窗</button>
    <button id="open2">打开天窗</button>
    
    <script>
        // 单例模式实现弹窗
        const Modal = (function () {
            let modal = null;
            
            return function () {
                if (!modal) { // 第一次创建实例
                    modal = document.createElement('div');
                    modal.id = 'modal';
                    modal.innerHTML = `
                        <span class="close-btn">&times;</span>
                        <div>我是一个全局唯一的Modal</div>
                    `;
                    modal.style.display = 'none';
                    document.body.appendChild(modal);
                    
                    // 绑定弹窗内部关闭按钮事件
                    modal.querySelector('.close-btn').addEventListener('click', function() {
                        modal.style.display = 'none';
                    });
                }
                return modal;
            }
        })();
        
        // 绑定外部按钮事件
        document.getElementById('open').addEventListener('click', function () {
            const modal = new Modal();
            modal.style.display = 'block';
        });
        
        document.getElementById('open2').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';
        });
    </script>
</body>

效果如下,当点击打开弹窗会显示弹窗,打开天窗还是保持原样,点击关闭,弹窗被隐藏

这个实现的核心是:

  1. 单例模式确保全局只有一个弹窗实例
  2. 延迟创建DOM,首次使用时才初始化
  3. 简单直接的显示 / 隐藏控制,没有复杂的交互逻辑

单例模式的优缺点:何时该用它?

(优点)

  1. 全局唯一性:确保整个应用中只有一个实例,便于数据共享和状态管理;
  2. 节约资源:避免重复创建实例,减少内存占用;
  3. 易于维护:全局访问点统一,修改时只需在一处改动。

(缺点)

  1. 违反单一职责原则:单例类负责创建自己的实例,又提供业务方法,职责过重;
  2. 扩展性差:单例类的结构通常比较固定,难以扩展;
  3. 单元测试困难:由于全局唯一,测试时可能影响其他测试用例。

单例模式的典型应用场景

单例模式在实际开发中非常常见,以下是一些典型场景:

  1. 配置管理:全局配置对象,确保所有地方使用相同的配置;
  2. 数据库连接:避免重复连接数据库,节省资源;
  3. 日志记录:统一的日志记录器,确保日志输出的一致性;
  4. 缓存系统:如 LocalStorage、SessionStorage 的封装,避免重复实例化;
  5. 全局状态管理:如 Redux 的 store,Vuex 的 store 等。

面试中如何优雅地实现单例模式?

面试时,除了实现基本功能,还可以考虑以下几点来展示你的专业性:

  1. 私有构造函数 :用private修饰构造函数(TypeScript 中),防止外部直接实例化;
  2. 线程安全:虽然 JS 是单线程,但可以提一下多线程环境下的解决方案(如双重检查锁定);
  3. 扩展性:讨论如何在单例基础上支持扩展(如工厂模式结合单例);
  4. 内存管理:单例实例通常不会被垃圾回收,需注意内存占用。
相关推荐
梦想CAD控件5 分钟前
在线CAD实现形位公差标注(在线编辑DWG)
前端·javascript·node.js
掘金一周6 分钟前
写个vite插件自动处理系统权限,降低99%重复工作 | 掘金一周 7.17
前端·人工智能·后端
爱编程的喵28 分钟前
React Fragment 深度解析:告别多余的 DOM 节点
前端·react.js
多啦C梦a30 分钟前
《hash+history》你点“关于”,页面却没刷新?!——揭秘前端路由的“穿墙术”
前端·javascript·面试
HHW32 分钟前
大文件上传难题?前端优雅解决方案全解析!
前端·node.js
蓝倾33 分钟前
淘宝获取商品规格接口(item-sku)操作详解
前端·后端·fastapi
水纹34 分钟前
使用pdfjs_3.2.146 预览并本地存储批注demo
前端·javascript
血舞之境34 分钟前
Android Gradle Plugin 7x 升级到 8.1 实战问题总结
前端
诺特健康前端组36 分钟前
解决element下拉框组件的filterable属性带来的选择完成后切屏再切回页面,下拉框会自动展开的BUG
前端
yvvvy36 分钟前
别再懵了!从小白到前端大厂选手的 History 路由通关指南(还顺带干翻 Hash)
javascript