JavaScript设计模式 -- 单例模式

发布于:2025-02-13 ⋅ 阅读:(11) ⋅ 点赞:(0)

在实际开发中,我们常常需要确保某个类只有一个实例,并提供全局访问点。**单例模式(Singleton Pattern)**正是为了解决这个问题而产生的。本文将详细介绍单例模式的原理、实现方式以及在 JavaScript 中的多种应用场景,通过多个示例代码,帮助你掌握如何在项目中使用单例模式。 

单例模式简介

单例模式的核心思想是保证一个类只有一个实例存在,并提供一个全局访问点来获取该实例。这样可以防止重复创建对象,节省资源,同时也便于全局共享数据或状态。

单例模式的特点:

  • 唯一性:在系统中只有一个实例存在。
  • 全局访问:提供一个全局访问点,让其他对象能够轻松地获取这个实例。
  • 延迟实例化:通常采用懒加载方式,只有在第一次使用时才创建实例。

JavaScript 中单例模式的实现方式

在 JavaScript 中,由于语言特性灵活,单例模式可以通过多种方式实现。下面介绍三种常见的实现方式。

基于对象字面量实现

最简单的单例实现方式就是使用对象字面量。适用于简单场景,不需要额外的初始化逻辑。

// 直接定义一个单例对象
const Singleton = {
  property: '这是一个单例属性',
  method() {
    console.log('调用单例方法');
  }
};

// 客户端调用
Singleton.method(); // 输出:调用单例方法
console.log(Singleton.property); // 输出:这是一个单例属性

基于闭包实现

利用闭包和立即执行函数表达式(IIFE),可以创建一个带有私有变量的单例实例,并实现延迟加载。

const Singleton = (function() {
  let instance; // 用于保存单例实例

  // 私有初始化函数
  function init() {
    // 私有变量和方法
    let privateData = '这是私有数据';
    function privateMethod() {
      console.log('调用私有方法');
    }
    return {
      // 公有方法和属性
      getPrivateData() {
        return privateData;
      },
      publicMethod() {
        console.log('调用公有方法');
        privateMethod();
      }
    };
  }

  return {
    // 获取单例实例的接口
    getInstance() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

// 客户端调用
const singletonA = Singleton.getInstance();
const singletonB = Singleton.getInstance();
console.log(singletonA === singletonB); // 输出:true
singletonA.publicMethod();
// 输出:
// 调用公有方法
// 调用私有方法

基于 ES6 类实现

利用 ES6 的 class 语法,可以在构造函数中判断是否已存在实例,从而实现单例模式。这种方式更符合面向对象的编程习惯。


class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    // 初始化操作
    this.timestamp = new Date();
    Singleton.instance = this;
  }

  getTime() {
    return this.timestamp;
  }
}

// 客户端调用
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出:true
console.log(instance1.getTime()); // 输出:实例创建时间


单例模式的应用场景

单例模式适用于那些需要全局唯一实例的场景,下面我们列举几个常见的例子。

配置管理器

在大型应用中,通常会有一套全局配置。使用单例模式可以确保配置管理器只有一个实例,方便统一管理和访问配置项。

class ConfigManager {
  constructor() {
    if (ConfigManager.instance) {
      return ConfigManager.instance;
    }
    this.config = {
      apiUrl: 'https://api.example.com',
      timeout: 5000
    };
    ConfigManager.instance = this;
  }

  get(key) {
    return this.config[key];
  }

  set(key, value) {
    this.config[key] = value;
  }
}

// 客户端调用
const config1 = new ConfigManager();
const config2 = new ConfigManager();
console.log(config1 === config2); // 输出:true
console.log(config1.get('apiUrl')); // 输出: https://api.example.com

日志记录器

日志记录器通常需要在整个应用中保持唯一实例,这样可以统一管理日志记录的逻辑、格式以及输出目的地。

class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    Logger.instance = this;
  }

  log(message) {
    console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
  }
}

// 客户端调用
const logger1 = new Logger();
const logger2 = new Logger();
logger1.log('系统启动'); // 输出带有时间戳的日志信息
console.log(logger1 === logger2); // 输出:true

全局事件总线

在前端项目中,全局事件总线用于组件间通信。采用单例模式可以保证整个应用中只有一个事件总线,从而避免事件冲突和重复创建问题。

class EventBus {
  constructor() {
    if (EventBus.instance) {
      return EventBus.instance;
    }
    this.events = {};
    EventBus.instance = this;
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(fn => fn(...args));
    }
  }
}

// 客户端调用
const eventBus1 = new EventBus();
const eventBus2 = new EventBus();
eventBus1.on('test', data => console.log('事件接收:', data));
eventBus2.emit('test', 'Hello, Singleton!'); // 输出:事件接收: Hello, Singleton!
console.log(eventBus1 === eventBus2); // 输出:true

数据库连接

在 Node.js 应用中,数据库连接通常资源昂贵且需要复用。使用单例模式可以确保数据库连接对象全局唯一,避免重复建立连接。

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    // 模拟数据库连接建立
    this.connection = this.connect();
    Database.instance = this;
  }

  connect() {
    // 模拟一个数据库连接
    console.log('建立数据库连接...');
    return { /* connection object */ };
  }

  getConnection() {
    return this.connection;
  }
}

// 客户端调用
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // 输出:true
db1.getConnection(); // 已建立的数据库连接

单例模式的优缺点

优点

  • 资源共享:确保系统中某个对象只有一个实例,便于共享资源(如配置、连接等)。
  • 全局访问:提供全局访问点,方便在不同模块中调用。
  • 避免重复实例化:节省资源,避免频繁创建和销毁对象。

缺点

  • 隐藏依赖:单例模式可能导致类之间隐式的依赖关系,不利于测试和扩展。
  • 并发问题:在多线程环境下需要额外处理线程安全问题(在 JavaScript 单线程环境中较少见)。
  • 难以继承:单例的实现可能会限制类的扩展性和灵活性。

总结

单例模式是一种简单但十分有用的设计模式,通过确保全局唯一实例,可以为配置管理、日志记录、事件总线、数据库连接等场景提供一致的解决方案。在 JavaScript 中,我们可以通过对象字面量、闭包以及 ES6 类等方式实现单例模式,选择哪种方式取决于具体需求和项目复杂度。希望本文的多种示例能够帮助你在实际开发中更好地理解和应用单例模式。

欢迎在评论区分享你的使用心得与疑问!