前端设计模式通关:单例与闭包的相爱相杀,从 Storage 封装看最佳实践

106 阅读5分钟

一、从「全局唯一」说起:单例模式的核心使命

1.1 为什么需要「单例」?

想象你有一个全局的存储管家,无论在哪个模块调用,都只能拿到同一个实例 —— 这就是单例模式的核心。在前端开发中,像 LocalStorage 封装、全局状态管理、日志服务等场景,都需要这种「唯一且全局可访问」的对象,避免重复实例化带来的资源浪费和数据混乱。
举个栗子🌰:如果每次操作 LocalStorage 都新建一个实例,就像每次打开冰箱都要重新买一台,显然不合理!单例模式就是让你拥有一台专属冰箱,随时取用。

1.2 ES6 类实现:优雅的静态控制法

class Storage {
  static instance = null; // 静态属性保存唯一实例
  constructor() {
    if (Storage.instance) { // 防止重复实例化
      throw new Error('请通过Storage.getInstance()获取实例~');
    }
    Storage.instance = this; // 首次实例化时保存自身
  }
  // 封装LocalStorage方法
  getItem(key) { return localStorage.getItem(key); }
  setItem(key, value) { localStorage.setItem(key, value); }
  // 静态方法提供全局访问点
  static getInstance() {
    return Storage.instance || (Storage.instance = new Storage());
  }
}
// 使用方式:全局唯一实例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true,稳稳的唯一实例

关键点解析

  • static instance作为类级别的存储容器,确保实例全局共享
  • 构造函数内的判空检查,从源头禁止「非法」实例化
  • getInstance静态方法实现「惰性初始化」,首次调用时才创建实例

1.3 ES5 闭包实现:复古但强大的作用域魔法

const Storage = (function() {
  let instance = null; // 闭包内的私有变量,外部无法直接访问
  function StorageBase() {
    if (instance) { // 同样防止重复创建
      return instance; // 直接返回已有实例
    }
    instance = this; // 首次调用时绑定实例
  }
  // 原型方法挂载,减少内存占用
  StorageBase.prototype.getItem = function(key) {
    return localStorage.getItem(key);
  };
  StorageBase.prototype.setItem = function(key, value) {
    localStorage.setItem(key, value);
  };
  // 返回工厂函数,控制实例创建
  return function() {
    if (!instance) {
      instance = new StorageBase(); // 严格遵循单例规则
    }
    return instance;
  };
})();
// 使用方式:通过调用函数获取实例
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2); // true,闭包确保实例唯一

闭包的关键作用

  • instance变量被封闭在 IIFE(立即执行函数)作用域内,形成私有空间
  • 返回的工厂函数作为访问入口,保持对instance的引用
  • 兼容旧环境,无需 ES6 语法也能实现严格单例

二、闭包:单例背后的「作用域守护者」

2.1 闭包的本质:被记住的「私有空间」

简单来说,闭包就是「函数 + 其引用的外部变量」的组合。当内部函数被保存到外部,它会记住创建时的作用域链,让外部无法直接访问的变量得以「间接访问」。
用生活化的例子理解🍔
闭包就像你点奶茶时的打包袋 —— 袋子(内部函数)不仅装着奶茶(函数逻辑),还带着你选的配料清单(外部变量),哪怕你把奶茶带回家,配料信息依然有效。

2.2 闭包如何助力单例?

在单例实现中,闭包主要解决两个问题:

  1. 私有变量保护:如 ES5 实现中的instance,外部无法直接修改,只能通过工厂函数访问
  2. 状态持久化:确保多次调用时,instance始终指向同一个实例,避免重复创建

2.3 闭包的双刃剑:优势与注意事项

优点注意事项
封装性强,保护内部状态过度使用可能导致内存泄漏(及时释放不再需要的引用)
实现私有方法和属性嵌套层级过深影响可读性(保持简洁设计)
支持惰性初始化避免滥用,非必要场景无需强制单例

三、进阶优化:让单例更「智能」

3.1 添加缓存机制:减少重复读取

// ES6版本优化,新增缓存功能
class Storage {
  static instance = null;
  constructor() {
    if (Storage.instance) return Storage.instance;
    Storage.instance = this;
    this.cache = new Map(); // 使用Map存储缓存数据
  }
  getItem(key) {
    if (this.cache.has(key)) { // 优先从缓存读取
      return this.cache.get(key);
    }
    const value = localStorage.getItem(key);
    this.cache.set(key, value); // 写入缓存
    return value;
  }
  setItem(key, value) {
    localStorage.setItem(key, value);
    this.cache.set(key, value); // 同时更新缓存
  }
}

优化价值:高频访问场景下减少 I/O 操作,提升性能,尤其适合移动端存储场景。

3.2 支持多种存储类型:localStorage/sessionStorage

// 增加存储类型配置,提升灵活性
class Storage {
  static instance = null;
  constructor(type = 'local') {
    if (Storage.instance) return Storage.instance;
    Storage.instance = this;
    this.storage = type === 'local' ? localStorage : sessionStorage;
  }
  // 统一接口,内部根据type调用对应存储API
  getItem(key) { return this.storage.getItem(key); }
  setItem(key, value) { this.storage.setItem(key, value); }
}
// 使用示例:创建sessionStorage单例
const sessionStorage = Storage.getInstance('session');

适用场景:根据业务需求灵活切换存储类型,保持接口一致性。

四、避坑指南:单例模式的正确打开方式

4.1 避免构造函数污染

错误做法:

// ❌ 错误!未在构造函数中阻止重复实例化
class BadStorage {
  static instance;
  constructor() { /* 无判空逻辑 */ }
}
const s1 = new BadStorage(); // 允许直接new,破坏单例
const s2 = new BadStorage(); // s1 !== s2,单例失效!

正确做法:始终在构造函数中检查instance,必要时抛出明确错误。

4.2 区分「单例」与「全局变量」

单例模式≠全局变量,核心区别:

  • 单例通过控制实例化过程保证唯一,支持惰性初始化
  • 全局变量是简单的全局对象,无法阻止重复赋值或篡改

4.3 适配模块化环境

在 ES6 模块中,单例可以更简洁:

// storage.js
class Storage { /* 单例实现 */ }
export default Storage.getInstance(); // 直接导出实例,确保唯一

使用时直接导入,无需每次调用getInstance,保持代码简洁。

五、总结:单例与闭包的最佳拍档

单例模式就像一个「全局管家」,确保核心资源唯一可控;闭包则是这位管家的「秘密仓库」,用作用域魔法保护关键状态。两者结合,既能实现优雅的封装,又能兼顾性能与灵活性。

下次遇到「全局唯一」需求时,记得:

  • ES6 场景优先用class + static实现,代码清晰易维护

  • 兼容场景使用闭包 + IIFE,复古但可靠

  • 复杂场景添加缓存、多类型支持等扩展功能,让单例更智能

掌握这对组合拳,无论是面试还是实战,都能轻松应对~ 代码的世界里,简洁与优雅永远是终极追求,而单例和闭包,正是通往这个目标的重要铺路石!