掌握单例模式:在 JavaScript 中实现实例的 “唯一法则”

409 阅读4分钟

前言

单例是什么?-----就是保证只有一个类,一个实例。

什么情况下需要单例模式呢?

假如你要做一个翻译软件,我们这里采用去问大模型的方式。我们给大模型一个 hello,然后这个大模型 model 就会返回给你翻译结果。

当你再问一次的时候,你会希望 model = new LLM() 在执行一次吗?加载一个大型语言模型可能需要占用数 GB 的内存,肯定是不想要的,所以这里就需要单例模式。让这个代码永远只有一个实例对象。

image.png

如何让一个类只有一个实例对象?

静态方法

我们在这创建一个类 SingleDog,

class SingleDog {
    show() {
        console.log('这是一个单例模式');
    }
}

const s1 = new SingleDog()
const s2 = new SingleDog()

console.log(s1 === s2) // falsse

注意:这里的s1和s2不相等 --- 引用地址不一样

我们想要让这个类只有一个实例对象,也就是我们希望这里 new 出来的实例对象是相等的。这是什么意思呢?

我们希望,new 多次,希望得到的一直是第一次的实例对象。

实现单例模式

这里就需要构造函数有一个能力了,构造函数需要知道自己有没有被调用过。

我这里提出一个方法,来看看这样可不可行:

//静态方法:只有构造函数能调用
class SingleDog {
    constructor() {
        this.count = 0  // 记录调用次数
        this.obj = null
    }
    show() {
        console.log('这是一个单例模式');
    }
    static getInstance() {
    console.log(this.count); // 调试  
    if (this.count > 0) {
        return this.obj // 第二次调用 retuen obj
    }
    this.count++
    this.obj = new SingleDog() // 把实例对象放到 obj
    return this.obj
}

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
console.log(s1 === s2)

结果是这样子的:false

image.png

为什么呢?

静态方法是直接在类上调用的,this 在这里并不指向实例,而是指向类本身,而 count 和 obj 是实例属性,而不是类的静态属性,所以类本身没有 count 和 obj 这两个实例属性。

那我们这里的 static getInstance() 可以改变一下:

class SingleDog {
    show() {
        console.log('这是一个单例模式');
    }
    static getInstance() {
        if (!SingleDog.instance) { // 在这个方法挂一个属性
            SingleDog.instance = new SingleDog
        }
        return SingleDog.instance
    }
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
console.log(s1 === s2)

或者说也可以沿用上一个思想:

class SingleDog {
    // 记录调用次数
    static count = 0;
    // 存储实例对象
    static obj = null;
    show() {
        console.log('这是一个单例模式');
    }
    static getInstance() {
        if (SingleDog.count > 0) {
            return SingleDog.obj;
        }
        SingleDog.count++;
        SingleDog.obj = new SingleDog();
        return SingleDog.obj;
    }
}

const s1 = SingleDog.getInstance();
const s2 = SingleDog.getInstance();
console.log(s1 === s2); 

闭包

//闭包
class SingleDog {
    show() {
        console.log('这是一个单例模式');
    }
}

SingleDog.getInstance = (function () {
    let instance = null;
    return function () {
        if (!instance) {
            instance = new SingleDog();
        }
        return instance;
    }
})();  // 自执行函数

const s1 = SingleDog.getInstance(); //调用的是 getInstance 里面的函数体
const s2 = SingleDog.getInstance();

console.log(s1 === s2);

getInstance 调用完,销毁掉会留下 instance 闭包,因为下面的函数执行上下文还需要 instance。

不懂闭包的uu可以看一下这一篇文章

html 实战

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <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 #000;
        }
    </style>
</head>

<body>
    <button id="open">打开弹窗</button>
    <button id="close">关闭弹窗</button>
    
    <script>
    </script>
</body>

</html>

我们这里希望,点击这个按钮可以打开和关闭弹窗。这里需要用到单例模式,因为你点击打开弹窗,会一直是之前的弹窗,而不会新建出一个新的弹窗。

image.png

来看看这里是怎么实现的:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <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 #000;
            transition: opacity 0.3s ease-in-out;
            /* 添加过渡效果,使元素的不透明度在 0.3 秒内平滑过渡 */
            opacity: 0;
            /* 初始状态下元素的不透明度为 0,即完全透明 */
        }
    </style>
</head>

<body>
    <button id="open">打开弹窗</button>
    <button id="close">关闭弹窗</button>
    <script>
        const Model = (function () {
            let model = null;
            // 内部函数,用于创建或显示/隐藏弹窗
            return function (flag) {

                if (!model) {    // 检查弹窗元素未创建
                    model = document.createElement('div');
                    model.innerHTML = '我是一个全局弹框';
                    model.id = 'model';
                    document.body.appendChild(model);
                }

                // 始终将元素的 display 属性设置为 block,确保元素存在但可能不可见
                model.style.display = 'block';
                // 根据传入的 flag 设置元素的不透明度,实现显示或隐藏
                model.style.opacity = flag === 'block' ? '1' : '0';
                // 返回创建或已存在的弹窗元素
                return model;
            }
        })();

        // 为打开弹窗的按钮添加点击事件监听器
        document.getElementById('open').addEventListener('click', () => {
            // 调用 Model 函数并传入 'block' 显示弹窗
            Model('block');
        });


        // 为关闭弹窗的按钮添加点击事件监听器
        document.getElementById('close').addEventListener('click', () => {
            // 调用 Model 函数并传入 'none' 隐藏弹窗
            Model('none');
        });
    </script>
</body>

</html>

用浏览器打开看,功能正常。

image.png

6fae22669fcb5a7344177a05b0ad047.png