手写单例模式:从LocalStorage封装看设计模式的魅力

115 阅读4分钟

大家好,我是你们的老朋友FogLetter,今天我们来聊聊前端开发中一个既基础又重要的设计模式——单例模式。这个模式看似简单,但里面藏着不少值得玩味的细节。让我们从一个实际案例出发,一步步揭开它的神秘面纱。

一、需求背景:封装LocalStorage

封装一个Storage类,基于LocalStorage实现,要求这个类必须是单例的,同时提供setItem(key, value)getItem(key)方法。

为什么需要单例?因为LocalStorage是浏览器提供的全局存储对象,我们不需要也不应该创建多个实例去操作它。单例模式能确保全局只有一个Storage实例,既节省资源,又避免数据混乱。

二、单例模式初探

单例模式(Singleton Pattern)是一种常用的设计模式,它保证一个类只有一个实例,并提供一个访问它的全局访问点

想象一下公司里的打印机——虽然很多人共用,但只需要一个实例就够了。如果每个员工都自己连接一台打印机,那办公室就乱套了!

在JavaScript中,实现单例有几种经典方式,今天我们先看两种最常用的:

1. ES6 Class实现方式

class Storage {
  static instance; // 静态属性,用于存储唯一实例
  
  // 静态方法,用于获取实例
  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); // true,确实是同一个实例

关键点解析:

  • static instance:静态属性,相当于"类级别"的变量,所有实例共享
  • static getInstance():静态方法,控制实例的创建
  • 判断if (!Storage.instance):确保只创建一次实例

这种实现方式清晰明了,充分利用了ES6的class特性。但有个小问题:我们仍然可以通过new Storage()来创建实例,这违反了单例原则。怎么解决呢?可以加上私有构造函数:

class Storage {
  static instance;
  
  // 私有构造函数
  constructor() {
    if (Storage.instance) {
      return Storage.instance;
    }
    Storage.instance = this;
  }
  
  // ...其他方法
}

2. 闭包实现方式

对于喜欢函数式编程的小伙伴,闭包方案可能更合胃口:

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

const Storage = (function() {
  let instance; // 闭包中的自由变量
  return function() {
    if (!instance) {
      instance = new StorageBase();
    }
    return instance;
  }
})();

// 添加原型方法
StorageBase.prototype.setItem = function(key, value) {
  localStorage.setItem(key, value);
};

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

// 使用
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2); // true

闭包方案的精妙之处:

  1. 立即执行函数(IIFE)创建了一个闭包环境
  2. instance变量被隐藏起来,外部无法直接访问
  3. 返回的函数充当了"构造函数",控制实例创建

这种方式虽然看起来"古老"一些,但它完美利用了JavaScript的函数作用域特性,实现了真正的私有变量,是很多流行库的惯用手法。

三、两种方案的对比

特性Class方案闭包方案
代码可读性高,结构清晰稍弱,需要理解闭包
私有性较弱强,真正私有
内存效率较高略低(闭包开销)
扩展性强,易于继承较弱
防止new创建需要额外处理天然支持

在实际项目中,如果使用现代JavaScript(ES6+),Class方案通常是首选;而在需要极致封装或维护老旧代码时,闭包方案依然很有价值。

四、单例模式的适用场景

单例模式在前端开发中随处可见:

  1. 全局状态管理:比如Redux的store
  2. 浏览器API封装:如这里的LocalStorage封装
  3. 对话框/模态框:通常整个应用只需要一个实例
  4. 日志记录器:统一记录应用日志
  5. 缓存系统:全局共享的缓存

但要注意,单例不是银弹!滥用单例会导致:

  • 代码耦合度高
  • 难以测试
  • 违反单一职责原则

五、总结

单例模式看似简单,实则内涵丰富。通过今天的探索,我们学到了:

  1. 单例模式的核心思想:一个类只有一个实例
  2. 两种主要实现方式:Class静态方法 vs 闭包
  3. 实际项目中的适用场景注意事项

记住,设计模式不是教条,而是解决问题的思路。理解背后的思想比死记硬背实现方式更重要!

如果觉得有收获,别忘了点赞收藏!我们下期再见~