从本地存储封装读懂单例模式:原来这就是设计模式的魅力

今天遇到了个有意思的问题,写了个操作 localStorage 的工具函数,在不同的模块里都调用了它,结果发现有时候存进去的数据,换个地方就读不出来了。仔细排查才发现,原来我在每个模块里都重新创建了一个操作实例,虽然都是操作 localStorage,但总感觉哪里不对劲。

翻了翻设计模式的资料,才恍然大悟 ------ 这种全局唯一的资源操作,正适合用单例模式来处理。单例模式其实是最容易理解的设计模式之一,今天就结合 localStorage 的封装,从基础概念到实际实现,跟大家好好说说它。

从问题出发:为什么需要单例模式?

先从那个 localStorage 工具说起。我最初的写法是这样的:

js 复制代码
class StorageUtil {
  setItem(key, value) {
    localStorage.setItem(key, value);
  }
  getItem(key) {
    return localStorage.getItem(key);
  }
  removeItem(key){
    localStorage.removeItem(key);
  }
  clear(){
    localStorage.clear();
  }
}

然后在不同模块里分别实例化:

js 复制代码
// 模块A
const storeA = new StorageUtil();
storeA.setItem('name', '张三');
// 模块B
const storeB = new StorageUtil();
console.log(storeB.getItem('name')); // 能读到,看似没问题

表面上看功能正常,但仔细想想:localStorage 本身是全局唯一的存储区域,为什么要为它创建多个操作实例呢?这就像给同一个门配了好几把钥匙,虽然都能开门,但完全没必要,甚至可能在复杂场景下引发问题 ------ 比如如果在实例里维护了缓存状态,多个实例就会导致状态不一致。

这时候就需要单例模式出场了。它的核心作用就是:保证一个类在整个应用中只有一个实例,并且提供一个全局访问点。这样既能避免重复创建实例造成的资源浪费,又能保证全局状态的一致性。

单例模式的核心定义与分类

单例模式是一种创建型 设计模式,它的核心定义可以概括为:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。简单来说,就是不管你尝试创建多少次,最终拿到的都是同一个对象。

根据实例创建的时机,单例模式主要分为两类:

1. 饿汉式单例------提前创建,随时待命

饿汉式单例会在类加载的时候就创建实例,不管后续是否会用到。这种方式的优点是实现简单,而且天然线程安全(在 Java 等多线程语言中),因为实例在程序启动阶段就已经初始化完成。

用 JavaScript 实现一个饿汉式的存储工具:

js 复制代码
class HungryStorage {
  // 类加载时就创建实例
  static instance = new HungryStorage();
  // 提供全局访问点
  static getInstance() {
    return HungryStorage.instance;
  }
  setItem(key, value) {
    localStorage.setItem(key, value);
  }
  getItem(key) {
    return localStorage.getItem(key);
  }
  removeItem(key){
    localStorage.removeItem(key);
  }
  clear(){
    localStorage.clear();
  }
}
// 无论调用多少次,拿到的都是同一个实例
const store1 = HungryStorage.getInstance();
const store2 = HungryStorage.getInstance();
console.log(store1 === store2); // true

这种方式的缺点也很明显:如果这个实例从始至终都没被使用,就会造成内存浪费。

2. 懒汉式单例------按需创建,节省资源

懒汉式单例会在第一次被使用的时候才创建实例,完美解决了饿汉式可能浪费资源的问题。但实现相对复杂一些,需要处理好实例创建的时机和线程安全问题(在多线程环境下)。

在前端开发中,我们更常用懒汉式单例,因为 JavaScript 是单线程执行(忽略 Web Worker 等特殊场景),不需要考虑多线程同步问题,实现起来更简单。

接下来看看在 JavaScript 中,懒汉式单例有哪些具体实现方式。

用 ES6 类实现懒汉式单例

ES6 的类语法提供了更清晰的面向对象实现方式,结合静态属性和静态方法,我们可以很方便地实现懒汉式单例。

js 复制代码
class Storage {
  // 静态属性存储唯一实例
  static instance;
  // 静态方法作为全局访问点
  static getInstance() {
    // 首次调用时创建实例,后续调用直接返回已有实例
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }
  // 实例方法封装localStorage操作
  setItem(key, value) {
    localStorage.setItem(key, value);
  }
  getItem(key) {
    return localStorage.getItem(key);
  }
  removeItem(key) {
    localStorage.removeItem(key);
  }
  clear() {
    localStorage.clear();
  }
}
// 测试:多次调用返回同一个实例
const store1 = Storage.getInstance();
const store2 = Storage.getInstance();
console.log(store1 === store2); // true

这里的关键是static关键字 ------ 静态属性instance属于类本身,而不是类的实例。getInstance方法会先检查是否已有实例,有就直接返回,没有才创建。这样不管调用多少次getInstance,拿到的都是同一个对象。

但后来我发现个小问题:如果有人直接new Storage(),还是能创建新实例。所以更严谨的做法是把构造函数私有化,但 JS 没有真正的私有构造函数,只能曲线救国:

js 复制代码
class Storage {
  static instance;
  constructor() {
    // 如果已有实例,直接返回已有实例
    if (Storage.instance) {
      return Storage.instance;
    }
    // 否则将当前实例赋值给静态属性
    Storage.instance = this;
  }
  // ... 其他方法同上
}
// 即使直接new,也只能拿到同一个实例
const store1 = new Storage();
const store2 = new Storage();
console.log(store1 === store2); // true

通过在构造函数中检查并返回已有实例,确保无论通过new还是getInstance,最终拿到的都是同一个对象。

闭包 + 立即执行函数

除了类的写法,利用 JavaScript 的闭包特性也能实现单例模式,这种方式更贴近函数式编程思想,实现也更隐蔽。

js 复制代码
// 定义基础操作类
function StorageBase() {}
StorageBase.prototype.setItem = function(key, value) {
  localStorage.setItem(key, value);
};
StorageBase.prototype.getItem = function(key) {
  return localStorage.getItem(key);
};
StorageBase.prototype.removeItem = function(key){
   localStorage.removeItem(key);
}

StorageBase.prototype.clear = function(){
   localStorage.clear();
}
// 用闭包控制实例创建
const Storage = (function() {
  let instance = null; 
  
  return function() {
    if (!instance) {
      instance = new StorageBase();
    }
    // 始终返回同一个实例
    return instance;
  };
})();
// 测试
const store1 = new Storage();
const store2 = new Storage();
console.log(store1 === store2); // true

这种实现的精髓在于立即执行函数(IIFE)创建的闭包环境:变量instance被封闭在私有作用域中,外部无法直接访问,只能通过返回的函数来操作。每次调用new Storage()时,都会先检查闭包中的instance是否存在,确保只创建一次实例。

对比类的实现,闭包版本的优势是不需要调用getInstance方法,直接new就能得到单例,使用起来更自然,而且实例的创建逻辑被完全隐藏,避免了被破坏的可能。

单例模式的实际应用场景

理解了实现方式,再看看前端开发中哪些场景适合用单例模式:

  1. 全局状态管理:Vuex 的 Store、Redux 的 createStore 都是典型的单例模式,整个应用只能有一个状态容器,否则状态就会混乱。

  2. 弹窗组件:页面中同一类型的弹窗(如提示框、登录框)通常只需要一个实例,重复创建会导致 DOM 冗余和状态冲突。

  3. API 请求实例:Axios 实例配置好 baseURL 和拦截器后,全局使用同一个实例更便于管理,避免重复配置。

  4. 工具类实例:如本文的 localStorage 操作工具、日志工具等,全局唯一实例能保证操作的一致性。

这些场景的共性是:需要全局唯一的访问点,或者重复创建实例会带来副作用

最后

单例模式看似简单,但其背后体现的是一种资源管理思想:对于全局唯一的资源,应该通过统一的入口进行访问和控制。

在 JavaScript 中,无论是用类的静态属性,还是用闭包,核心都是通过一个 "容器"(静态属性或闭包变量)保存实例,并在创建时检查这个容器是否已有值。两种实现方式没有绝对优劣,类的写法更符合面向对象思维,闭包写法更具 JavaScript 特色,根据实际场景选择即可。 希望本文让你对单例模式有更清晰的了解~

相关推荐
augenstern41619 分钟前
HTML面试题
前端·html
张可19 分钟前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课1 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
谢尔登1 小时前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js
蓝婷儿2 小时前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我2 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
OpenTiny社区2 小时前
告别代码焦虑,单元测试让你代码自信力一路飙升!
前端·github
kk_stoper2 小时前
如何通过API查询实时能源期货价格
java·开发语言·javascript·数据结构·python·能源
pe7er2 小时前
HTTPS:本地开发绕不开的设置指南
前端
晨枫阳2 小时前
前端VUE项目-day1
前端·javascript·vue.js