JS `Singleton Pattern`:确保类只有一个实例,全局共享资源

各位观众老爷,大家好!我是你们的老朋友,今天咱们聊聊JavaScript里的“单身贵族”——单例模式(Singleton Pattern)。

开场白:为啥要有单例?

想象一下,你是个皇帝,只有一个玉玺,盖章生效。如果突然冒出俩玉玺,那谁说了算?国家还不乱套了!单例模式就是保证,对于某些特别重要的类,我们只能有一个实例,确保全局只有一个入口,避免混乱。

单例模式是啥?

简单来说,单例模式就是限制一个类只能创建一个实例,并且提供一个全局访问点。这个访问点通常是一个静态方法,让你随时随地都能拿到这个唯一的实例。

为啥要用单例?

  • 资源控制: 只有一个实例,意味着资源占用可控。比如,数据库连接,全局缓存,日志对象等等,共享一个实例可以节省资源。
  • 数据一致性: 只有一个实例,所有操作都针对同一个对象,保证数据的一致性。
  • 全局访问: 方便访问,不需要到处传递对象,直接通过单例类的静态方法就可以拿到实例。
  • 配置管理: 全局配置对象,方便读取和修改配置信息。

单例模式的几种实现方式

接下来咱们来撸起袖子,写几个JavaScript版本的单例模式。

1. 饿汉式单例 (Eager Initialization)

这种方式最简单粗暴,类加载的时候就创建好实例,天生就是单例。

class Singleton {
  static instance = new Singleton(); // 类加载时就创建实例

  constructor() {
    if (Singleton.instance) {
      throw new Error("Singleton class cannot be instantiated directly. Use getInstance() method.");
    }
    // 初始化操作
    this.data = "Hello from Singleton";
  }

  static getInstance() {
    return Singleton.instance;
  }

  getData() {
    return this.data;
  }
}

// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData());      // Hello from Singleton

优点: 实现简单,线程安全(JavaScript是单线程)。

缺点: 无论是否使用,实例都会被创建,浪费资源。

2. 懒汉式单例 (Lazy Initialization)

这种方式比较懒,只有在第一次使用的时候才创建实例。

class Singleton {
  static instance = null;

  constructor() {
    if (Singleton.instance) {
      throw new Error("Singleton class cannot be instantiated directly. Use getInstance() method.");
    }
    // 初始化操作
    this.data = "Hello from Singleton";
  }

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }

  getData() {
    return this.data;
  }
}

// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData());      // Hello from Singleton

优点: 延迟加载,节省资源。

缺点: 在某些并发环境下可能存在线程安全问题(虽然在JavaScript中几乎可以忽略,但还是要注意)。

3. 使用闭包实现的单例

利用JavaScript的闭包特性,将实例保存在闭包中,防止被外部访问。

const Singleton = (function() {
  let instance;

  function createInstance() {
    // 初始化操作
    return {
      data: "Hello from Singleton (Closure)",
      getData: function() {
        return this.data;
      }
    };
  }

  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData());      // Hello from Singleton (Closure)

优点: 隐藏了实现细节,避免外部直接修改实例。

缺点: 代码稍微复杂一些。

4. 使用ES6的Symbol实现单例

利用Symbol的唯一性,防止外部通过new关键字创建实例。

const SINGLETON_KEY = Symbol('singleton');

class Singleton {
  constructor(token) {
    if (token !== SINGLETON_KEY) {
      throw new Error('Cannot construct Singleton directly, use getInstance() method');
    }
    // 初始化操作
    this.data = "Hello from Singleton (Symbol)";
  }

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton(SINGLETON_KEY);
    }
    return Singleton.instance;
  }

  getData() {
    return this.data;
  }
}

Singleton.instance = null;

// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true,指向同一个实例
console.log(instance1.getData());      // Hello from Singleton (Symbol)

// 尝试直接new Singleton()会报错
// const instance3 = new Singleton(); // Error: Cannot construct Singleton directly, use getInstance() method

优点: 更强的封装性,防止外部直接创建实例。

缺点: 代码稍微复杂一些。

5. TypeScript中的单例

TypeScript可以更好地利用private构造函数来保证单例。

class Singleton {
    private static instance: Singleton | null = null;

    private constructor() {
        // 初始化操作
        this.data = "Hello from Singleton (TypeScript)";
    }

    static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }

    getData(): string {
        return this.data;
    }

    private data: string;
}

// 使用
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

console.log(instance1.getData()); // Hello from Singleton (TypeScript)

优点: 利用TypeScript的类型系统, 保证更好的代码质量和可维护性. private constructor 更加安全.

缺点: 需要使用TypeScript.

各种单例实现方式对比

实现方式 优点 缺点 适用场景
饿汉式 实现简单,线程安全(JS单线程) 浪费资源,实例在类加载时就创建,无论是否使用 对资源占用不敏感,且实例必须存在的场景
懒汉式 延迟加载,节省资源 在某些并发环境下可能存在线程安全问题(JS几乎可以忽略) 对资源占用敏感,且实例不是必须立即存在的场景
闭包 隐藏实现细节,避免外部直接修改实例 代码稍微复杂 需要隐藏实现细节,防止外部直接访问实例的场景
Symbol 更强的封装性,防止外部直接创建实例 代码稍微复杂 需要更强封装性,防止外部通过new关键字创建实例的场景
TypeScript 利用TypeScript的类型系统, 保证更好的代码质量和可维护性, private constructor 更加安全. 需要使用TypeScript. 需要使用TypeScript, 并且希望利用类型系统提供更安全单例实现的场景.

单例模式的应用场景

  • 配置管理: 将配置信息保存在单例对象中,方便全局访问。
  • 数据库连接池: 维护一个数据库连接池单例,避免频繁创建和销毁连接。
  • 日志对象: 使用单例的日志对象,统一记录日志信息。
  • 任务队列: 单例的任务队列,保证任务按顺序执行。
  • 全局缓存: 使用单例的缓存对象,存储全局数据。
  • 用户会话管理: 在服务端,可以使用单例来管理用户会话信息。

单例模式的注意事项

  • 过度使用: 不要滥用单例模式,只在真正需要全局唯一实例的场景下使用。
  • 测试: 单例模式可能会增加单元测试的难度,需要注意测试策略。
  • 状态管理: 单例对象的状态是全局共享的,需要注意状态的管理,避免出现意外的副作用。
  • 模块化: 在模块化开发中,可以使用模块来代替单例模式,提供更好的封装性和可测试性。

一些“不正经”的单例使用场景

  • 后宫佳丽三千,皇上只有一个! (虽然可能有很多皇子,但皇上只有一个)
  • 一个网站的管理员账号,只有一个! (当然,可以有多个权限不同的管理员,但最高权限的那个只有一个)
  • 一个班的班长,只有一个! (副班长不算!)

总结

单例模式是一种常用的设计模式,可以保证一个类只有一个实例,并提供全局访问点。在JavaScript中,有多种方式可以实现单例模式,选择哪种方式取决于具体的应用场景和需求。希望通过今天的讲解,大家能够对单例模式有更深入的理解,并在实际开发中灵活运用。

记住,单例虽好,可不要贪杯哦!合理使用才能发挥它的最大价值。

最后的彩蛋

其实,在JavaScript中,很多内置对象都是单例的,比如window对象,document对象等等。它们都是全局唯一的,可以直接访问。

好了,今天的讲座就到这里,感谢大家的收看!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注