JavaScript 单例模式详解:确保类的唯一实例

117 阅读5分钟

在前端开发中,设计模式是解决特定问题的最佳实践。本文将深入探讨单例模式(Singleton Pattern),它是最简单且实用的设计模式之一。


什么是单例模式?

单例模式的定义:确保一个类只有一个实例存在,那就要求它的构造方法一定不能是public公开的,即不能被外界实例化。那它的构造方法只能是private, 并且拥有一个当前类的静态成员变量,后面他要求向整个系统提供这个实例,即我们要再提供一个静态的方法,向外界提供当前类的实例,当前实例只能在内部进行实例化,不能够放到外面去。


为什么需要单例模式?

在实际开发中,某些对象我们只需要一个实例,例如:

  • 浏览器中的 localStorage 对象

  • 全局状态管理

  • 配置管理器

  • 数据库连接池

创建多个实例不仅浪费资源,还可能导致数据不一致的问题。


 单例模式的实现方式

 1. 对象字面量实现单例

这是最简单也是最常见的单例写法,直接通过一个对象字面量定义一个唯一的对象。

const Singleton = {
  name: "Singleton",
  doSomething() {
    console.log("Doing something...");
  }
};

// 使用
Singleton.doSomething(); // Doing something...
  • 这种方式直接暴露了所有属性和方法,无法做到私有性。
  • 适用于不需要复杂逻辑的场景。

2. 使用闭包封装私有变量

我们可以通过 IIFE(立即执行函数表达式)+ 闭包来创建一个带有私有状态的单例。

const Singleton = (function () {
  let instance;

  function init() {
    // 私有变量
    const privateVar = "I'm private";

    return {
      publicMethod() {
        console.log(privateVar);
      }
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

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

console.log(instance1 === instance2); // true,同一个实例
instance1.publicMethod(); // I'm private
  • 利用闭包实现了对 privateVar 的封装。
  • 通过 getInstance() 控制唯一实例的创建。
  • 第一次调用时创建实例,后续调用返回缓存的实例。

3. 使用 ES6 类 + 静态方法实现单例

ES6 引入了 class,虽然不能直接限制构造函数只被调用一次,但我们可以在类中模拟单例行为。

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = "Singleton Data";
    Singleton.instance = this;
  }

  getData() {
    return this.data;
  }
}

// 使用
const s1 = new Singleton();
const s2 = new Singleton();

console.log(s1 === s2); // true
console.log(s1.getData()); // Singleton Data
  • 在构造函数中检查是否已经存在实例,若存在则返回已有实例。
  • 利用了类的静态属性保存实例。
  • 注意这种方式依赖于开发者自觉使用 new 来获取实例。

如果你对于上述的代码实例还存在疑惑,让我们接下来看到下述代码片段加深学习

ES6 类实现单例模式


普通类的实例化问题

让我们先看一个普通的 Storage 类:

class Storage {
  constructor() {
    console.log(this, '~~~');
  }
  
  getItem(key) {
    // 获取数据
  }
  
  setItem(key, value) {
    // 设置数据
  }
}

const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2, '~~~'); // false

上面的代码每次使用 new 关键字都会创建一个新的实例,导致 storage1storage2 是不同的对象。这在某些场景下可能会导致问题,比如多个实例操作同一个资源时的数据不一致。

使用静态方法实现单例模式

ES6 的 class 语法提供了 static 关键字,可以用来实现单例模式:

class Storage {
  static instance;
  
  constructor() {
    console.log(this, '~~~');
  }
  
  // 静态方法
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }
  
  getItem(key) {
    return localStorage.getItem(key);
  }
  
  setItem(key, value) {
    return localStorage.setItem(key, value);
  }
}

const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2, '~~~'); // true
storage1.setItem('name', '卢老板');
console.log(storage1.getItem('name'), '~~~'); // 卢老板
console.log(storage2.getItem('name'), '~~~'); // 卢老板

这里的关键点是:

  1. 使用 static instance 作为静态属性保存唯一实例
  2. 提供 static getInstance() 方法检查实例是否存在:
    • 如果不存在,则创建新实例
    • 如果存在,则返回已有实例

. 通过 getInstance() 方法而非直接使用 new 来获取实例

使用闭包实现单例模式

除了 ES6 的 class 语法,我们还可以使用函数闭包来实现单例模式:

function StorageBase() {
  // 构造函数
}

StorageBase.prototype.getItem = function(key) {
  return localStorage.getItem(key);
};

StorageBase.prototype.setItem = function(key, value) {
  return localStorage.setItem(key, value);
};

// 使用闭包实现单例模式
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, '~~~'); // true

storage1.setItem('name', 'Mr Liu');
console.log(storage1.getItem('name'), '~~~'); // Mr Liu
console.log(storage2.getItem('name'), '~~~'); // Mr Liu

这种实现方式的关键点是:

  1. 使用立即执行函数(IIFE)创建闭包环境
  2. 在闭包中保存 instance 变量,确保实例的唯一性
  3. 返回一个函数,该函数检查实例是否存在并返回

单例模式的优势

  1. 资源节约:只创建一个实例,减少内存占用
  2. 避免冲突:确保对共享资源的一致访问
  3. 全局访问点:提供统一的访问入口,方便管理
  4. 延迟初始化:实例在首次使用时才被创建(懒加载)

单例模式的实际应用

封装 localStorage 操作

正如我们的示例所示,单例模式非常适合封装浏览器的 localStorage API:

class StorageManager {
  static instance;
  
  static getInstance() {
    if (!StorageManager.instance) {
      StorageManager.instance = new StorageManager();
    }
    return StorageManager.instance;
  }
  
  getItem(key) {
    return localStorage.getItem(key);
  }
  
  setItem(key, value) {
    return localStorage.setItem(key, value);
  }
  
  removeItem(key) {
    return localStorage.removeItem(key);
  }
  
  clear() {
    return localStorage.clear();
  }
}

// 使用
const storage = StorageManager.getInstance();
storage.setItem('user', JSON.stringify({name: '张三', age: 25}));

单例模式的注意事项

  1. 避免过度使用:不是所有对象都需要单例模式,过度使用会增加代码耦合度
  2. 测试难度:单例对象的状态在测试用例之间可能相互影响
  3. 并发问题:在多线程环境中需要考虑线程安全(JavaScript是单线程的,不存在这个问题)

总结

单例模式是一种简单但强大的设计模式,它确保一个类只有一个实例,并提供全局访问点。在JavaScript中,可以通过ES6的static关键字或闭包来实现单例模式。

单例模式适用于需要协调行为的场景,如管理共享资源、状态管理等。在实际开发中,我们应该根据具体需求选择是否使用单例模式,避免过度使用导致系统耦合度过高。