【JavaScript】设计模式——单例模式

75 阅读5分钟

一、函数类与 class 类的基础认知 📚

在 JavaScript 里,类就像制作饼干的模具,能造出一堆长得一样的饼干(对象)。我们有两种 “模具”:函数类和 ES6 新出的 class 类。

1. 函数类

用构造函数就能定义函数类,既能收参数,又能挂公共方法,还能搞个 “静态方法” 玩玩

比如这样:

function Point(x, y) {
  this.x = x; // 给实例加个x属性
  this.y = y; // 再给实例加个y属性
}

// 原型上挂个实例方法,大家都能用
Point.prototype.toString = function () {
  return `(${this.x}, ${this.y})`;
};

// 直接在函数上定义的,就是静态方法,实例拿不到
Point.toSum = function (a, b) {
  return a + b;
};

// 造个实例试试
const point1 = new Point(1, 2);
console.log(point1.toString()); // 输出:(1, 2) ✨
console.log(Point.toSum(3, 4)); // 输出:7 ✨
console.log(point1.toSum); // 输出:undefined(实例访问不到静态方法)

这里的Point就是函数类,new一下就能造实例。this指向新实例,prototype上的是实例方法,函数上直接定义的是静态方法,只能用类名调用

2. class 类

ES6 的class类就是个 “语法糖”,把构造函数和原型包装得更好看啦,写起来超直观

看例子:

class Point {
  // 构造器,造实例时自动调用,用来初始化
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  // 实例方法,其实也是挂在原型上的
  toString() {
    return `(${this.x}, ${this.y})`;
  }
  
  // 静态方法,加个static关键字,只能类名调用
  static toSum(a, b) {
    return a + b;
  }
}

// 造个实例玩玩
const point2 = new Point(3, 4);
console.log(point2.toString()); // 输出:(3, 4) ✨
console.log(Point.toSum(5, 6)); // 输出:11 ✨
console.log(point2.toSum); // 输出:undefined(实例还是拿不到静态方法)

class里的constructor就是构造器,toString是实例方法,static修饰的toSum是静态方法,和函数类功能差不多,但看起来更清爽

二、从生活场景理解单例模式 🏠

你家的总电闸,是不是只有一个?多装几个就乱套了!😅

编程里也一样,有些对象就该独一份!比如全局配置、弹窗管理器、购物车,要是整出好几个实例,不仅费资源,数据还可能打架。这种 一个类只能造一个实例 的思路,就是单例模式

三、单例模式的核心特点 🔑

就仨特点,记牢咯:

  1. 唯一实例:不管造多少次,都返回同一个对象
  1. 自行实例化:自己管自己的创建,不用外人插手
  1. 全局访问:有个统一的入口能拿到这个唯一实例

代码效果大概这样:

// 假设有个单例类Singleton
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // 输出:true(俩变量指向同一个实例)

四、用 JS 实现单例模式 💻

实现方式有好几种,从简单到复杂来看看

1. 基础实现:用闭包存实例

先整个函数类的:

function Singleton() {
  // 看看有没有实例了
  if (Singleton.instance) {
    return Singleton.instance;
  }
  // 没有就造一个
  this.name = "单例实例";
  this.version = "1.0.0";
  // 存起来
  Singleton.instance = this;
}

// 试试
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true ✨
console.log(a.name); // "单例实例"
console.log(b.version); // "1.0.0"

2. 基于 ES6 class 的实现

用 class 写更清楚:

class Singleton {

  // 实例方法
  addData(item) {
    this.data.push(item);
  }
  
  // 静态方法,提供全局访问入口
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

// 获取实例
const c = Singleton.getInstance();
const d = Singleton.getInstance();

console.log(c === d); // true ✨
c.addData("test");
console.log(d.data); // ["test"](数据共享)

3. 更优雅的实现:立即执行函数

用闭包把实例藏起来,更安全:

const Singleton = (function() {
  let instance; // 闭包里的变量存实例
  class InnerSingleton {
    constructor() {
      this.config = {
        theme: "light",
        language: "zh-CN"
      };
    }
    updateConfig(key, value) {
      this.config[key] = value;
    }
  }
  // 返回个函数用来拿实例
  return function() {
    if (!instance) {
      instance = new InnerSingleton();
    }
    return instance;
  };
})();

// 用用看
const e = new Singleton();
const f = new Singleton();
e.updateConfig("theme", "dark");
console.log(f.config.theme); // "dark"(数据同步更新啦)
console.log(e === f); // true ✨

五、单例模式的优缺点分析 ⚖️

优点:

  1. 省资源:就一个实例,不用频繁创建销毁,省性能
  1. 数据一致:都操作同一个实例,不会出现数据乱套的情况
  1. 全局能访问:在哪都能拿到,用着方便

缺点:

  1. 隐藏依赖:全局都能改,说不定哪就被改了,不好查
  1. 测试麻烦:实例状态会影响测试,前一个测试改了数据,下一个可能就不准了
  1. 不专一:又管创建又管业务,违背单一职责原则
  1. 难扩展:就一个实例,想扩展或替换都费劲

六、单例模式的适用场景 📍

虽然有缺点,但这些场景用它超合适:

  1. 全局配置对象:整个应用就一份配置,比如接口地址、主题设置
// 全局配置单例
class Config {
  constructor() {
    if (Config.instance) return Config.instance;
    this.apiBaseUrl = "https://api.example.com";
    this.timeout = 5000;
    Config.instance = this;
  }
}
  1. 弹窗管理器:整个页面就一个管理器管弹窗显示隐藏
class DialogManager {
  constructor() {
    if (DialogManager.instance) return DialogManager.instance;
    this.dialogs = [];
    DialogManager.instance = this;
  }
  showDialog(content) {
    // 显示弹窗的逻辑
  }
}
  1. 购物车实例:电商网站,购物车必须全局唯一
class ShoppingCart {
  constructor() {
    if (ShoppingCart.instance) return ShoppingCart.instance;
    this.items = [];
    ShoppingCart.instance = this;
  }
  addItem(product) {
    this.items.push(product);
  }
}
  1. 日志工具:一般就一个日志实例统一处理日志输出

七、使用单例模式的注意事项 ⚠️

  1. 别乱用:不是啥都要搞成单例,确实需要唯一实例再用
  1. 管好状态:单例状态会一直存在,没用的数据及时清掉
  1. 注意并发:多线程环境(JS 用 Worker 时)要加锁保证唯一
  1. 测试隔离:写单元测试时,每个测试前后最好重置下单例状态

八、总结 📝

单例模式就是保证一个类只有一个实例,还能全局访问。省资源、数据一致是它的好处,但也有隐藏依赖、测试难等问题。

需要全局唯一对象协调系统时,再用单例模式!比如全局配置、弹窗管理,用它能让代码更清爽高效

希望这篇文章能让你搞懂单例模式,下次遇到类似需求,就能轻松搞定!🎉