localStorage 单身日记:如何让一个 Storage 对象拒绝"再婚"

57 阅读5分钟

JavaScript 单例模式实践:用 Class 和闭包封装 Storage

单例模式是 JavaScript 中最常用的设计模式之一,它确保一个类只有一个实例,并提供一个全局访问点。本文将详细介绍如何使用 ES6 Class 和闭包两种方式实现单例模式,并封装一个基于 localStorage 的 Storage 工具。

什么是单例模式?

单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。这在需要全局状态管理或避免重复创建相同功能的对象时非常有用。

单例模式的特点:

  • 一个类只有一个实例
  • 必须自行创建这个实例
  • 必须向整个系统提供这个实例

在前端开发中,单例模式常用于:

  • 全局状态管理
  • 缓存系统
  • 对话框、弹窗等UI组件
  • 与浏览器API(如localStorage)交互的工具类

使用 ES6 Class 实现 Storage 单例

让我们先用 ES6 Class 实现一个基于 localStorage 的单例 Storage 工具:

javascript

class Storage {
  // 静态属性,用于存储唯一实例
  static instance = null;

  // 私有构造函数,防止外部直接实例化
  constructor() {
    if (!Storage.instance) {
      // 初始化操作
      this.store = window.localStorage;
      Storage.instance = this;
    }
    return Storage.instance;
  }

  // 静态方法,用于获取单例实例
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }

  // 封装 localStorage 的 setItem 方法
  setItem(key, value) {
    try {
      if (typeof value === 'object') {
        value = JSON.stringify(value);
      }
      this.store.setItem(key, value);
    } catch (e) {
      console.error('Storage setItem error:', e);
    }
  }

  // 封装 localStorage 的 getItem 方法
  getItem(key) {
    try {
      let value = this.store.getItem(key);
      try {
        value = JSON.parse(value);
      } catch (e) {
        // 如果不是 JSON 字符串,直接返回原值
      }
      return value;
    } catch (e) {
      console.error('Storage getItem error:', e);
      return null;
    }
  }
// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();

console.log(storage1 === storage2); // true,确实是同一个实例

storage1.setItem('name', '张三');
console.log(storage2.getItem('name')); // '张三'

代码解析

  1. 静态属性 instance:用于存储类的唯一实例,初始为 null。
  2. 构造函数:检查是否已存在实例,如果不存在则创建并存储到 instance 中。这样确保无论调用多少次 new Storage(),都返回同一个实例。
  3. 静态方法 getInstance() :提供获取单例的标准方法,这是单例模式的典型实现方式。
  4. 封装 localStorage 方法:对原生 localStorage 的方法进行封装,增加了错误处理和 JSON 自动转换功能。
  5. 使用示例:展示了如何获取实例并验证单例特性,以及基本的使用方法。

使用闭包实现 Storage 单例

对于更喜欢函数式编程的开发者,可以使用闭包来实现同样的功能:

javascript

const Storage = (function() {
  let instance = null;
  let store = null;

  function init() {
    // 私有方法
    function setItem(key, value) {
      try {
        if (typeof value === 'object') {
          value = JSON.stringify(value);
        }
        store.setItem(key, value);
      } catch (e) {
        console.error('Storage setItem error:', e);
      }
    }

    function getItem(key) {
      try {
        let value = store.getItem(key);
        try {
          value = JSON.parse(value);
        } catch (e) {
          // 如果不是 JSON 字符串,直接返回原值
        }
        return value;
      } catch (e) {
        console.error('Storage getItem error:', e);
        return null;
      }
    }

    function removeItem(key) {
      store.removeItem(key);
    }
    return {
      setItem,
      getItem,
      removeItem
    };
  }

  return {
    getInstance: function() {
      if (!instance) {
        store = window.localStorage;
        instance = init();
      }
      return instance;
    }
  };
})();

// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();

console.log(storage1 === storage2); // true

storage1.setItem('age', 25);
console.log(storage2.getItem('age')); // 25

代码解析

  1. 立即执行函数 (IIFE) :创建一个闭包环境,保护私有变量 instance 和 store 不被外部访问。
  2. init 函数:初始化真正的存储功能,包含所有私有方法。
  3. 返回对象:只暴露 getInstance 方法,确保单例特性。
  4. 使用示例:与 Class 版本相同,验证单例特性和基本使用方法。

Class 与闭包实现的比较

特性Class 实现闭包实现
可读性更清晰,结构更直观需要理解闭包概念
私有性需要约定(如 _ 前缀)天然的私有性
扩展性易于继承和扩展较难扩展
内存效率稍高(原型继承)稍低(每个方法都是新的)
现代 JavaScript 特性使用最新语法使用传统模式

单例模式的优缺点

优点

  1. 严格控制实例数量:确保全局唯一实例,避免资源浪费。
  2. 全局访问点:方便在应用各处访问同一实例。
  3. 延迟初始化:只有在第一次使用时才创建实例。

缺点

  1. 全局状态:可能导致代码耦合度高,难以测试。
  2. 违反单一职责原则:单例类既管理自己的生命周期又负责业务逻辑。
  3. 内存泄漏风险:如果单例持有大量数据,可能不会及时释放。

实际应用中的注意事项

  1. 线程安全:在 JavaScript 单线程环境中不需要考虑,但在其他语言中需要注意。
  2. 序列化问题:如果需要序列化单例对象,需要考虑如何保持单例特性。
  3. 测试困难:单例的全局状态可能使单元测试变得复杂,可以考虑依赖注入。
  4. 过度使用:不要滥用单例模式,只有在真正需要唯一实例时才使用。

总结

单例模式是一种强大而常用的设计模式,特别适合管理全局状态和封装浏览器API。本文介绍了两种实现方式:

  1. ES6 Class 实现:更现代、更直观,适合大多数现代JavaScript项目。
  2. 闭包实现:更函数式,提供更好的封装性,适合喜欢函数式编程的开发者。

在封装 localStorage 时,我们不仅实现了单例模式,还增加了错误处理、JSON自动转换和过期时间等实用功能,使这个Storage工具更加健壮和实用。

记住,设计模式是工具而不是目标,选择最适合你项目需求和团队习惯的实现方式才是最重要的。单例模式虽好,但也要避免滥用,特别是在需要多实例或测试驱动开发的场景中。