设计模式创建型之单例模式

实验介绍

本实验主要介绍了设计模式中的单例模式,在前端领域中,有很多地方都运用到了单例模式的思维,例如目前的主流前端框架中所用到的 Redux 和 Vuex 。实验首先通过一个小例子为大家展示了单例模式的实现原理,随后通过完成一个自定义的 Storage 存储器来帮助大家加深对单例模式的理解。最后为大家简单介绍了 Vuex 的基本原理,展示了单例模式的真实应用场景。希望大家在本小节实验中有所收获。

知识点

  • 单例模式介绍
  • 如何实现一个 Storage 存储器
  • 单例模式的应用之 Vuex 和 Redux
  • 实验挑战

单例模式介绍

一个类仅有一个实例?没错,当你想控制实例的数量时,可以考虑单例模式。

我们可以先举两个例子:

  • Windows 是多进程多线程的,但是在操作一个文件的时候,必然只能让一个进程(线程)来控制,否则会出现同一个文件被多个进程(线程)修改,导致文件修改异常的情况。
  • windows 的回收站是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例,试想一下,如果回收站不是唯一的,那你删除的文件就可能很难被复原了,因为你很有可能不知道文件被放在了哪个回收站。

在代码实现过程中,当你有一个类被频繁地创建与销毁时,这难免会造成系统资源的浪费。

或者是你对某个类实例化后,对这个实例对象进行了一些操作,例如存入了大量数据。而此时在其他地方你也需要使用这个数据,然而你又不能再实例化一个,因为新实例化的是没有之前实例对象中的数据的。

这种情况下你只能想办法在全局使用同一个实例对象。到后面你会发现这实际上就是 Vuex 和 Redux 的实现原理,后面会详细讲这一部分。

因此我们需要明确的一点是:如果要实现单例模式,那么创建实例的类就要具备判断全局范围是否已经创建过实例的能力,像下面这样就是不行的:

在实验环境中新建 index.html 文件与 singlePeople1.js 文件,并在 index.html 中引入 singlePeople1.js

随后在 singlePeople1.js 中添加如下代码:

// singlePeople1.js

class SinglePeople {
  constructor(name) {
    this.name = name;
  }
  eat = () => {
    console.log(`${this.name}可以吃东西!`);
  };
}

const person1 = new SinglePeople("张三");
const person2 = new SinglePeople("张三");

console.log(person1 === person2); // false

可以看到,虽然我们本意是想创建张三这个人,但是通过 SinglePeople 创建的实例却是不一样的,由此表明这样的创建方式是无法实现单例模式的。

那么我们就需要一种方式来帮助类来“记忆”它已经创建过的实例,因此我们给类增加一个属性 instance ,用于判断是否已经存在实例,就像这样:

创建 singlePeople2.js ,并在 index.html 中引入:

// singlePeople2.js

class SinglePeople {
  constructor(name) {
    this.name = name;
  }

  static getInstance(name) {
    // 判断是否已经创建过实例
    if (!SinglePeople.instance) {
      // 实例不存在则创建
      SinglePeople.instance = new SinglePeople(name);
    }
    // 实例存在则直接返回
    return SinglePeople.instance;
  }

  eat = () => {
    console.log(`${this.name}可以吃东西!`);
  };
}

现在让我们来试验一下:

// singlePeople2.js

const person1 = SinglePeople.getInstance("张三");
const person2 = SinglePeople.getInstance("张三");

console.log(person1 === person2); // true

可以看到,即使我们创建了两次实例,然而两次的实例却是一样的,于是可以知道通过这样方式是可以实例单例模式的。

至此,通过这个例子,相信各位同学应该能对单例模式有一个大体的概念,即一个类仅有一个实例,并提供一个访问它的全局访问点,即使重复的通过该类创建实例,也只会返回已经被创建过的实例。

接下来,我们会通过实现一个基于 localStorage 全局的的存储器,来看一看在前端中如何应用单例模式。

实现一个 Storage 存储器

在前端的项目中,有一些数据是全局任何地方都有可能用到的,因此需要把这样的数据以某种方式保存起来,并且支持在任何文件中都能调用到。

在本小节中,我们将通过 localStorage 来实现一个全局的 Storage 存储器,它运用单例模式的思维,提供了唯一的全局访问点,可以读取数据和存放数据,以便于一些常用的数据能被方便的调用到。

先创建一个 myStorage.js 文件,并引入 index.html 中。

首先我们需要构建一个 MyStorage 类,其核心之处在于,需要该类具有这样的能力:实例不存在则创建,已创建则直接返回已创建过的实例。

就像这样:

// myStorage.js

class MyStorage {
  // 核心部分,实例不存在则创建,已创建则直接返回
  static getInstance = () => {
    if (!MyStorage.instance) {
      MyStorage.instance = new MyStorage();
    }
    return MyStorage.instance;
  };
}

同样的,我们来测试一下是否能够满足上面的要求:

// myStorage.js

const storage1 = MyStorage.getInstance();
const storage2 = MyStorage.getInstance();

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

可以看到,该 MyStorage 类的确能提供唯一的全局访问点。

但此时还未结束,我们还需要为它添加读取数据和保存数据的能力,于是为 MyStorage 添加 setStategetState 方法:

// myStorage.js

class MyStorage {
  // 核心部分,实例不存在则创建,已创建则直接返回
  static getInstance = () => {
    if (!MyStorage.instance) {
      MyStorage.instance = new MyStorage();
    }
    return MyStorage.instance;
  };

  // 保存数据:通过 key/value 的形式传参
  setState(key, value) {
    window.localStorage.setItem(key, value);
  }

  // 通过 key 读取数据
  getState(key) {
    return window.localStorage.getItem(key);
  }

  // 为了方便查看数据,添加展示全部 localStorage 中数据的函数
  show = () => {
    console.log(window.localStorage);
  };
}

可以看到,setStategetState 方法本质也是通过调用 localStorage 的内置 API 来实现的,但这的确提供了我们所需要的能力。

接下来就让我们来测试一下这个 MyStorage 类是否能满足我们的需求:

第一步:创建一个实例,并查看当前存储情况

// myStorage.js

const store1 = MyStorage.getInstance();
store1.show(); // Storage {length: 0}

可以看到,该存储器暂时没有存放任何数据。

第二步:测试添加数据和读取数据的能力

// myStorage.js

store1.setState("name", "张三");
store1.setState("age", 24);
store1.setState("sex", "男");
store1.show(); // Storage {age: '24', name: '张三', sex: '男', length: 3}
console.log(store1.getState("name")); // 张三

通过打印输出,可以看到该实例的添加数据和读取数据功能都没有问题。

第三步:再创建一个实例,判断是否会重复创建

// myStorage.js

const store2 = MyStorage.getInstance();
console.log(store1 === store2); // true
store2.show(); // Storage {age: '24', name: '张三', sex: '男', length: 3}

从打印结果来看,store2 store1 实际上是同一个实例,其保存的数据是一样的

第四步:为 store2 中放入新值,观察 store1 的数据

// myStorage.js

store2.setState("work", "程序员");

store1.show(); // Storage {work: '程序员', name: '张三', age: '24', sex: '男', length: 4}
store2.show(); // Storage {work: '程序员', name: '张三', age: '24', sex: '男', length: 4}

从这一步的结果可以看到,store2 和 store1 打印结果相同,证明它们确实是同一个实例。

至此,这个简单的小例子就讲完了,相信同学们通过这里例子能更好的了解单例模式的定义与应用。

那么在当前的前端领域中,有哪些真正运用到单例模式的呢?

Redux 和 Vuex 相信大部分同学都有了解,或者了解其中的一种。它们实际上就是前端框架对单例模式的经典应用,如果你不了解也没有关系,在下一小节中,我们会对二者进行一个简单的介绍。

单例模式的应用之 Vuex 和 Redux

谈起 Rudex 和 Vuex ,首先要讲一讲它们的由来。

虽然这二者之间有差别,但在单例模式的应用层面是一致的,考虑到国内使用 Vue 的同学偏多,接下来就以 Vuex 为例( Redux 类似 )。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。(摘自 vuex 官网)

对于没有实际使用过 Vuex 的同学而言,上面一句话并不是很容易理解,我们先来看看它能做什么。

在 Vue 开发过程中,对于某个状态,或者说某个值需要被很多组件用到时,一般会面对两个问题:

  1. 多个视图依赖于同一状态;
  2. 来自不同视图的行为需要变更同一状态。

对于第一个问题,当你的项目足够简单时,你完全可以采用组件的逐层传参,然而当项目复杂起来,你会发现这样的方式非常不方便,当你修改其中某一环组件,可能会影响整个传参路线。同时不能处理兄弟间状态传递,只能处理父子间状态传递。

对于第二个问题,同样是当项目复杂起来时会让代码维护和修改变得异常艰难,且同样无法处理兄弟间的状态。尽管可以通过状态提升的方式,但这样无疑会导致嵌套更多的组件。

这里对状态提升做个简单的解释:当你需要在兄弟间组件中使用同一状态时,你可以将此状态放在他们的父组件中,然后传进需要的子组件;当你需要修改时,也只能通过在子组件中调用父组件中修改状态的方法。

回想我们对单例模式的定义:一个类仅有一个实例,并提供一个访问它的全局访问点。

而 Vuex 正是使用了这样的思想:它把组件的共享状态抽取出来,以一个全局单例模式进行管理。在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

这里为大家抽取了一下源码中关于单例模式的关键代码:

let Vue;
// ...

export function install(_Vue) {
  // 这里就是和我们上面创建单例的思路一样
  // 首先判断是否已经创建过了唯一的 vuex 实例,创建过则直接返回
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== "production") {
      console.error(
        "[vuex] already installed. Vue.use(Vuex) should be called only once."
      );
    }
    return;
  }
  // 若没有,则为这个创建一个唯一的 Vuex
  Vue = _Vue;
  // 将 Vuex 的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue);
}

你可以清晰的看到,虽然具体的代码实现不同,但是其本质是一样的:若已经存在类的实例则不再重新创建,不存在时才创建一个新的实例。

注:如果你仍然对这一块不太清楚,希望可以去 Vuex 官网系统学习一下它,或者在报告中留言你不清楚的地方。( Vuex 官网

这里仅仅用 Vuex 进行举例,Redux 也是类似的。实际上,Vuex 在设计上借鉴了 Redux,二者的本质都在于,运用单例模式的思想,构建了一个全局单例模式的 object tree 中,以便于在各个层级的组件中使用。

这就是单例模式在前端领域中较为经典的应用场景。

实验挑战

利用单例模式的思想实现登录框的显示和隐藏。

提示:可以使用控制台打印的方式模拟登录框是在显示状态还是隐藏状态。

答案代码放在实验最后课程的源码包里,大家可以自行下载,和自己的实现方式进行比对。

小结及下节实验内容预告

至此,关于单例模式的介绍基本告一段落了,单例模式的定义相较于简单,希望大家能够认真学习,真正掌握。

小结一下:在单例模式中,需要牢记两点:

  1. 一个类仅有一个实例,并提供一个访问它的全局访问点。
  2. 理解这段代码:
static getInstance = () => {
  if (!MyStorage.instance) {
    MyStorage.instance = new MyStorage();
  }
  return MyStorage.instance;
}

下一节实验会为大家带来原型模式,并且会着重介绍原型及原型链,理解好原型不仅能帮助大家学习好原型模式,还能加深对 javascript 的理解。

原型与原型链不容易理解,下一节会尽全力为大家理清其概念,不过仍然希望各位能在学习下一节实验之前先自行在网上搜索一下相关内容,进行预习,这样在进行下一节实验的学习时能做到心中有数,也能更快、更好的理解。

实验总结

本节实验为大家展示了单例模式在前端领域的应用场景,我们说学习要知其然,更要知其所以然。以往大家在使用 Vuex 或 Redux 的过程中,也许并没有意识到这是单例模式的思想。通过本小节实验的学习帮助大家了解它们的实现原理,在以后的使用过程中,能够做到心中有数。

本节实验源码压缩包下载链接:单例模式源码

posted @   雨晨*  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示