单例模式:前端开发者的"唯一钥匙"🔑

867 阅读4分钟

🌱 基础篇:什么是单例模式?

image.png

🧠 想象场景:你和朋友在奶茶店点单,如果每来一个顾客就开一家分店,会出现什么问题?

// ❌ 错误示范:多实例地狱
const branch1 = new Storage(); 
const branch2 = new Storage(); 

branch1.setItem('爱珍珠还是爱乡乡', '爱乡乡'); 
console.log(branch2.getItem('爱珍珠还是爱乡乡')); // 猜猜会输出什么?🤯

💡 答案揭晓
由于不是单例模式,branch2会创建一个新的"珍珠库存",导致数据不一致!就像两家奶茶店各自进货,顾客永远不知道哪家有珍珠😅

🎯 正确做法

// ✅ 单例模式保障
const mainStore = Storage.getInstance(); 
mainStore.setItem('爱珍珠还是爱乡乡', '爱乡乡'); 
console.log(Storage.getInstance().getItem('爱珍珠还是爱乡乡')); // 100%显示'爱乡乡'!

🌟 单例模式三要素

  1. 唯一性:全局只有一个实例(就像奶茶店总店)
  2. 全局访问点:通过Storage.getInstance()获取(类似保险箱的钥匙)
  3. 延迟初始化:首次调用时才创建实例(节省内存)

🔧 进阶篇:两种实现方式大比拼

image.png

方式一:构造函数方式

class Storage {
  constructor() {
    if (Storage.instance) {
      return Storage.instance; // ⚠️ 关键点:重复调用返回已有实例
    }
    this.storage = window.localStorage;
    Storage.instance = this; // ✅ 保存唯一实例
  }
}

方式二:静态方法方式

class Storage {
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage(); // 🔄 只创建一次
    }
    return Storage.instance;
  }
}

🧠 对比分析

特性构造函数方式静态方法方式
实例创建时机第一次new时第一次调用getInstance时
内存效率优秀优秀
代码可读性中等(需注意new的陷阱)高(明确的工厂方法)

🧠 深度解析:饿汉式 vs 懒汉式

image.png

饿汉式(Eager Initialization)

定义:类加载时立即创建实例
优点

  • 线程安全(无需额外同步)
  • 实现简单,性能高
    缺点
  • 可能造成资源浪费(即使未被使用)
class Singleton {
  constructor() {
    this.data = "Singleton Data";
  }

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

// 饿汉式:类加载时立即创建
Singleton.instance = new Singleton();

懒汉式(Lazy Initialization)

定义:首次调用 getInstance() 时才创建实例
优点

  • 资源利用率高(按需加载)
    缺点
  • 多线程环境下需处理线程安全(JS中通常无需考虑)
class Singleton {
  constructor() {
    this.data = "Lazy Singleton Data";
  }

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

🧠 对比分析

特性饿汉式懒汉式
实例创建时机类加载时立即创建首次调用 getInstance() 时创建
线程安全安全(无需额外同步)需要额外处理(JS中通常无需)
资源占用可能浪费(即使未使用)按需加载,节省资源
适用场景实例创建成本低,且必用实例创建成本高,或可能不被使用

🧪 实战篇:LocalStorage封装实战

image.png

🛠️ Storage.js完整实现

class Storage {
  constructor() {
    if (Storage.instance) {
      return Storage.instance;
    }
    this.storage = window.localStorage;
    Storage.instance = this;
  }

  setItem(key, value) {
    try {
      const jsonValue = JSON.stringify(value); // 💡 自动序列化
      this.storage.setItem(key, jsonValue);
      return true;
    } catch (error) {
      console.error("存储失败:", error);
      return false;
    }
  }

  getItem(key) {
    try {
      const value = this.storage.getItem(key);
      if (value === null) return null;
      return JSON.parse(value); // 🔄 自动反序列化
    } catch (error) {
      console.error("读取失败:", error);
      return this.storage.getItem(key);
    }
  }

  // 其他方法...
}

export default new Storage(); // 🚨 导出单例实例!

🧭 使用示例

// ✅ 正确写法
import storage from './Storage.js';

storage.setItem('user', { name: '乡乡', age: 20 });
console.log(storage.getItem('user').age); // 20

// ❌ 错误写法
const s1 = new Storage(); 
const s2 = new Storage(); 
console.log(s1 === s2); // false!双胞胎打架了💥

🚨 常见陷阱:这些坑千万别踩!

image.png

陷阱1:误用new关键字

// ❌ 多实例地狱
const s1 = new Storage(); 
const s2 = new Storage(); 
console.log(s1 === s2); // false!数据不一致⚠️

陷阱2:忘记导出单例

// ❌ 错误导出
export class Storage { ... }

// ✅ 正确导出
export default new Storage();

陷阱3:未处理JSON转换

// ❌ 存储对象会变成"[object Object]"
storage.setItem('user', user);

// ✅ 正确方式(自动处理)
storage.setItem('user', { name: '小红' });

🛠️ 验证机制:如何证明是单例?

image.png

🧪 单例验证测试

const s1 = Storage.getInstance();
const s2 = Storage.getInstance();
console.log(s1 === s2); // true ✅

// 查看内存地址(Chrome开发者工具)
console.log(s1);
console.log(s2);

🖥️ Chrome开发者工具操作指南

  1. 打开开发者工具(F12)
  2. 切换到Application标签页
  3. 在LocalStorage中查看数据
  4. 比较不同实例的存储内容

🧠 读者思考环节

image.png

思考题1:猜猜输出什么?

const s = Storage.getInstance();
s.setItem('count', 10);
console.log(s.getItem('count')); // ?

思考题2:如何修复这个错误?

function initApp() {
  const s = new Storage(); // ❌ 错误写法
  s.setItem('theme', 'dark');
}

思考题3:为什么推荐静态方法?

// 选项A: new Storage()
// 选项B: Storage.getInstance()
// 选项C: Storage()

📚 附录:完整运行示例

example.html

<!DOCTYPE html>
<html>
  <body>
    <script type="module">
      import storage from './Storage.js';
      
      // 测试单例
      const s1 = storage;
      const s2 = storage;
      console.log(s1 === s2); // true
      
      // 测试数据存储
      s1.setItem('test', { key: 'value' });
      console.log(s2.getItem('test')); // { key: 'value' }
    </script>
  </body>
</html>

🌟 总结:单例模式的价值

image.png

优势生活类比
数据一致性奶茶店统一库存
内存高效一台咖啡机服务所有人
全局访问公司唯一打卡机
易于维护单一管理系统

通过本教程,你已经掌握了:

  • 单例模式的核心思想(唯一钥匙🔑)
  • 两种实现方式的对比(工厂模式 vs 构造函数)
  • 饿汉式与懒汉式的实现与区别
  • LocalStorage的优雅封装
  • 避免常见陷阱的技巧

现在,快去你的项目中实践这些知识吧!记得用Storage.getInstance()而不是new Storage()哦~ 😄