今天就让我们来聊聊关于设计模式中的单例模式。所谓单例模式,就是保证一个类只有一个实例,也就是类的实例是唯一的,但它可以与多个其他对象进行交互或被多个其他对象使用。
举个例子:假设我们要写一个翻译的app,我们在代码中引入一个大模型,我们输入一个'hello'让它翻译,于是就去调用这个大模型生成一个实例对象来翻译'hello',再输入'world'又调用大模型生成一个实例对象,每次输入都会生成一个实例对象,这样就会浪费资源使用,但是如果我们一开始生成一个实例对象,后面输入都用这个实例对象来操作,这样就不会因为重复创建实例而浪费资源了,这就要使用到设计模式中的单例模式了。
class
我们先来介绍一下 JS 中的class类,它比构造函数更强大,它包括了构造函数,可以当作构造函数来使用。
先来回顾一下构造函数,如下;
Person.prototype.like = '泡脚'
Person.say = function () {
return 'hello'
}
function Person() {
this.name = '梁总'
this.age = 18
}
let p = new Person()
console.log(p); // Person { name: '梁总', age: 18 }
console.log(p.like); // 泡脚
console.log(p.say); // undefined
console.log(Person.say()); //hello
定义了一个Person的构造函数(开头字母一般大写),其显示拥有name和age俩个属性,在 Person 原型上定义了一个属性like,这样就使得new出来的实例对象p隐式拥有 Person 原型上的方法,而Person.say是一个静态方法,直接附加在构造函数 Person 上,而不是 Person 的原型上。
console.log(p.say) 尝试访问实例p上的say方法,但是say是作为静态方法定义在构造函数上的,不是原型链上的方法,所以输出为:undefined,而console.log(Person.say()) 正确地调用了静态方法say,输出为:hello。
class 语法是 ES6 引入的一种新语法,称构造函数的语法糖(使得原型链的写法更加简洁和清晰)。既然class类能够当作构造函数来使用,那我们就用一个 class 来完全代替上面的构造函数。
class Person {
constructor(name, age) {
this.name = '梁总'
this.age = 18
}
like() {
return `${this.name}爱泡脚`
}
static say() {
return 'hello'
}
}
let p = new Person()
console.log(p); // Person { name: '梁总', age: 18 }
console.log(p.like()); // 梁总爱泡脚
console.log(p.say); // undefined
console.log(Person.say()); //hello
这样就清晰多了,定义一个Person类,construct相当于构造函数,实例对象显示拥有的属性,想让其隐式拥有一个属性后就可以直接写在类里(如like()),不想让实例对象拥有一个属性就前面加关键字static(如上面代码中的say()),但是这个static有什么作用呢?别急,下面的单例模式就可以用到了。其中需要注意like()方法中的this因为是由实例对象p调用,触发隐式绑定,this指向实例对象p。
单例模式
前面说到,单例模式就是确保一个类只有一个实例,无论被调用多少次,操作的都是这个实例。
先写一个简单的class类,调用俩次该class类。
class SingleDog {
show() {
console.log('我是一个单身狗');
}
}
let s1 = new SingleDog()
let s2 = new SingleDog()
console.log(s1 === s2); // false
这样看显然操作的不是同一个实例,可以使用console.log(s1 === s2);判断是不是同一个实例。但是怎么样才可以使它除了第一次是创建一个实例,后面再调用类操作的是之前那个实例呢?
于是就需要在class类上挂一个静态属性(存储唯一实例),既然要判断是否之前已经存在,就要保存在一个地方,为了符合封装的理念,且只保存在类本身上,这时候static就能派上用场了。我们可以在SingleDog类内部添加一个静态static属性(只有这个类能用)来存储这个唯一的实例。当再次调用创建时,检查这个静态属性是否已经有了一个实例,有了则返回这个实例,没有创建一个新的实例并将其存储在静态属性中。
如下:
static instance; // 静态属性,用于存储类的唯一实例,外部不应该直接访问这个属
static getInstance() {
if (!SingleDog.instance) {
SingleDog.instance = new SingleDog();
}
return SingleDog.instance;
}
也就是在类上创建一个实例(静态方法可以被类本身调用,也可以被类的实例调用),然后在外部通过调用SingleDog.getInstance() 来获取实例。
总之,单例模式的关键点是:
- 一个静态属性用于存储唯一实例。
- 一个静态方法用于获取这个实例,如果实例不存在,就创建它。
- 构造函数通常是私有的或受保护的(static),以防止外部直接实例化类。
具体如下:
class SingleDog {
// 静态属性,用于存储类的唯一实例,外部不应该直接访问这个属性
static instance;
// 类的公共方法
show() {
console.log('我是一个单身狗');
}
// 静态方法,用于获取类的唯一实例
static getInstance() {
if (!SingleDog.instance) {
// 如果没有实例,则创建一个新的实例并将其赋值给静态属性
SingleDog.instance = new SingleDog();
}
// 返回存储在静态属性中的实例
return SingleDog.instance;
}
}
// 获取SingleDog的唯一实例
let s1 = SingleDog.getInstance();
let s2 = SingleDog.getInstance();
console.log(s1 === s2); // 输出: true
但是上面的方法会改变类的原始结构(加了一个静态属性instance),我们还可以使用闭包来实现,在外部定义一个getInstance方法来控制实例的创建,getInstance函数内部使用闭包来存储instance(存储类的唯一实例),如下。
class SingleDog {
show() {
console.log('我是一个单身狗');
}
}
// SingleDog.getInstance()() 自执行函数 第一个括号放的是function
SingleDog.getInstance = (function () {
let instance = null // 存储类的唯一实例
return function () {
if (!instance) {
instance = new SingleDog()
}
return instance
}
})()
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
console.log(s1 === s2); // true
我们在外部往SingleDog类上挂一个getInstance函数,在函数内部定义一个变量instance来保存第一次new出来实例,再让返回出来的函数访问它,这就形成了一个闭包可供它访问。接下来就是判断是否已经被创建了。这样就可以确保无论getInstance被调用多少次,都只会创建一个SingleDog实例了。
应用场景
在生活中,有许多场景都会运用到单例模式,比如在一个应用中,通常只需要一个登录窗口。使用单例模式可以确保无论用户点击多少次登录按钮,都只会弹出一个登录窗口,避免了多个登录窗口同时出现的情况。
那我们就来个简单的例子应用一下单例模式。俩个按钮,一个打开弹窗,一个关闭弹窗,要求点击打开弹窗会弹出一个弹窗,关闭后会取消该弹框。弹窗的样式,按钮如下;
<style>
#model {
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
}
</style>
<body>
<button id="open">打开弹窗</button>
<button id="close">关闭弹窗</button>
</body>
首先给按钮绑定点击事件,我想要点击打开后会显示弹框,点击关闭后会取消弹框,于是希望有一个功能来控制弹窗是否显示,如下;
document.getElementById('open').addEventListener('click', () => {
Model('block')
});
document.getElementById('close').addEventListener('click', () => {
Model('none');
});
然后封装一个函数用来将弹框加入到页面上以及控制里面的内容,打开关闭就可以接收一个参数flag然后再在函数里面设置dispaly属性控制它显示还是隐藏就可以以实现效果了,如下。
<script>
const Model = function (flag) {
let div = document.createElement("div");
div.innerHTML = '我是一个全局弹框'
div.id = 'model';
div.style.display = flag;
document.body.appendChild(div);
}
</script>
好,逻辑看上去没毛病,这时候看效果:
点击打开会添加一个display:block弹框,再点击又会添加一个display:block弹框,点取消也会添加一个display:none的弹框,明显,不符合预期,因为每次点击都添加了一个div,取消也添加一个弹框只不过为display:none。
我们需要的只是一个div,打开或关闭都在这个div上操作才对,所以就需要用到单例模式了,只能有一个div,且每次操作都在这个div上操作。
<script>
// const Model=()() 立即执行函数表达式 第一个括号放的是function
const Model = (function () {
let model = null
return function (flag) {
if (!model) {
model = document.createElement("div");
model.innerHTML = '我是一个全局弹框'
model.id = 'model';
document.body.appendChild(model);
}
model.style.display = flag;
return model
}
})()
</script>
这样你就可以一直在一个实例上操作了,怎么样,你学会了吗?感觉文章对你有所帮助可以点点赞o。