单例模式实现登录弹窗:延迟加载 + 全局唯一

101 阅读5分钟

前言

之前介绍了单例模式实现Storagejuejin.cn/post/752720… ,今天我们再来详细聊一下单例模式来实现弹窗功能。

在 Web 开发中,登录弹窗(Modal) 是一个非常常见的交互组件。为了提升用户体验和页面性能,我们希望:

  • 不跳转页面,直接在当前页面弹出登录框;
  • 不提前加载,只在用户点击“登录”时才创建;
  • 只创建一次,避免重复渲染和资源浪费;
  • 全局唯一,多个按钮都能访问到同一个弹窗实例。

这就非常适合使用 单例模式(Singleton Pattern) 来实现。

本文目标

我们将使用 三种方式 来实现登录弹窗的单例模式:

  1. 闭包 + IIFE(立即执行函数)
  2. 类(class) + 静态属性
  3. 模块模式(Module Pattern)

并通过一个统一的 HTML 页面来演示它们的使用和效果。

 基础 HTML 结构(统一使用)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>登录弹窗单例实现</title>
  <style>
    #modal {
      width: 300px;
      height: 200px;
      line-height: 200px;
      text-align: center;
      position: fixed;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border: 1px solid #000;
      background: #fff;
      z-index: 999;
      display: none;
    }
    #mask {
      width: 100%;
      height: 100%;
      position: fixed;
      left: 0;
      top: 0;
      background: rgba(0,0,0,0.5);
      z-index: 998;
      display: none;
    }
  </style>
</head>
<body>
  <button id="open1">打开弹窗 - 方式一</button>
  <button id="open2">打开弹窗 - 方式二</button>
  <button id="open3">打开弹窗 - 方式三</button>
  <button id="close">关闭弹窗</button>

  <script>
    // 我们将在这里分别实现三种方式
  </script>
</body>
</html>

方法一:闭包 + IIFE(最简洁的单例实现)

🧩 实现逻辑:

  • 使用 IIFE 创建一个私有作用域;
  • 使用闭包保存 modal 实例;
  • 只在第一次调用时创建弹窗;
  • 后续调用返回同一个实例。

🧩 实现代码

const ModalSingleton1 = (function () {
  let modal = null; // 闭包中的私有变量,用于存储唯一的弹窗实例
  return function () {
    if (!modal) { // 只有当 modal 为 null 时才创建新实例
      modal = document.createElement('div');
      modal.id = 'modal';
      modal.innerHTML = '我是一个 Modal(方式一)';
      document.body.appendChild(modal);
      console.log('方式一:创建了弹窗'); // 第一次调用时打印日志
    }
    return modal; // 返回已存在的或新创建的唯一实例
  };
})();

关键在于:通过闭包保留了 modal 的引用,并且在首次调用时创建并返回该实例。后续的所有调用都会直接返回这个已创建的实例,从而实现了单例模式。

优点:

  • 简洁、直观;
  • 利用闭包实现私有变量;
  • 不依赖类语法,兼容性好。

 方法二:类(class)+静态属性(面向对象的写法)

🧩 实现逻辑:

  • 使用 class 定义弹窗类;
  • 使用静态属性 _instance 保存实例;
  • 构造器中判断是否已存在实例;
  • 如果存在则返回,否则创建。

🧩 实现代码:

class ModalSingleton2 {
  static _instance = null; // 静态属性,用于存储唯一的实例

  constructor() {
    if (ModalSingleton2._instance) { // 如果已有实例,则直接返回该实例
      return ModalSingleton2._instance;
    }
    this.modal = document.createElement('div');
    this.modal.id = 'modal';
    this.modal.innerHTML = '我是一个 Modal(方式二)';
    document.body.appendChild(this.modal);
    console.log('方式二:创建了弹窗'); // 第一次创建实例时打印日志

    ModalSingleton2._instance = this; // 将当前实例赋值给静态属性
  }

  getModal() {
    return this.modal; // 提供一个公共方法获取模态框实例
  }
}

关键在于:使用 静态属性 _instance 来保存唯一的实例,这样无论创建多少个对象实例,都指向同一个 _instance。在构造器中检查 _instance 是否存在,如果存在则直接返回该实例,否则创建一个新的实例并赋值给 _instance

优点:

  • 更符合现代 JS 的面向对象风格;
  • 可扩展性强(可以加方法、状态);
  • 逻辑清晰,适合大型项目。

方法三:模块模式—— 模拟模块化封装

🧩 实现逻辑:

  • 使用 IIFE 创建一个模块;
  • 内部维护私有变量 modal
  • 对外暴露 show() 和 hide() 方法;
  • 实现统一访问接口。

🧩 实现代码:

const ModalSingleton3 = (function () {
  let modal = null; // 私有变量,用于存储唯一的弹窗实例

  function createModal() {
    modal = document.createElement('div');
    modal.id = 'modal';
    modal.innerHTML = '我是一个 Modal(方式三)';
    document.body.appendChild(modal);
    console.log('方式三:创建了弹窗'); // 第一次创建实例时打印日志
  }

  return {
    show() {
      if (!modal) createModal(); // 如果没有实例,则创建新的
      modal.style.display = 'block'; // 显示弹窗
    },
    hide() {
      if (!modal) createModal(); // 即使是隐藏操作,也要确保实例存在
      modal.style.display = 'none'; // 隐藏弹窗
    }
  };
})();

关键在于:模块模式提供了良好的封装性,所有内部状态都被隐藏在模块内部,对外仅暴露必要的接口(如 show()hide())。

这种方式非常适合工具类或插件开发,因为它能够有效地管理内部状态并提供简洁的外部接口。

优点:

  • 封装性强,对外只暴露必要的接口
  • 适合做工具类模块;
  • 可扩展为配置化弹窗。

绑定按钮事件(统一测试)

document.getElementById('open1').addEventListener('click', () => {
  const modal = new ModalSingleton1();
  modal.style.display = 'block';
});

document.getElementById('open2').addEventListener('click', () => {
  const modal = new ModalSingleton2().getModal();
  modal.style.display = 'block';
});

document.getElementById('open3').addEventListener('click', () => {
  ModalSingleton3.show();
});

document.getElementById('close').addEventListener('click', () => {
  const modal1 = new ModalSingleton1();
  modal1.style.display = 'none';

  const modal2 = new ModalSingleton2().getModal();
  modal2.style.display = 'none';

  ModalSingleton3.hide();
});

效果图: image.png

📌 总结对比表

方法实现方式是否使用类适用场景优点
闭包 + IIFE函数闭包快速原型、小型项目简洁、轻量
类 + 静态属性class(面向对象写法)中大型项目、面向对象开发结构清晰、可扩展
模块模式IIFE + 暴露接口工具类封装、配置化组件接口统一、封装性强

 总结

单例模式的本质是“全局唯一 + 延迟加载”,我们可以用闭包、类或模块模式来实现它。不同方式适合不同项目结构和开发习惯,理解它们的逻辑和适用场景,有助于写出更优雅、更健壮的代码。