单例模式在前端开发中其实很常见?手把手教你封装一个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. 内存管理:单例实例通常不会被垃圾回收,需注意内存占用。
相关推荐
吃杠碰小鸡4 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone10 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
Serene_Dream28 分钟前
JVM 并发 GC - 三色标记
jvm·面试
xjt_090129 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农41 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js