JavaScript设计模式精讲:单例模式从入门到实战

183 阅读5分钟

什么是单例模式?

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个访问该实例的全局访问点

想象一下,你正在开发一个系统,需要一个全局的配置管理器。无论在系统的哪个地方访问这个配置管理器,你都希望它是同一个实例,这就是单例模式的典型应用场景。

单例模式的实现方式

1. 基础实现

class ConfigManager {
    static instance = null;
    constructor() {
        if (ConfigManager.instance) {
            return ConfigManager.instance;
        }
        this.config = {};
        ConfigManager.instance = this;
    }
    
    setConfig(key, value) {
        this.config[key] = value;
    }
    
    getConfig(key) {
        return this.config[key];
    }
}

// 使用示例
const config1 = new ConfigManager();
const config2 = new ConfigManager();

console.log(config1 === config2); // true,证明是同一个实例

2. 闭包实现(更安全的方式)

const ConfigManager = (function() {
    let instance = null;
    
    class Config {
        constructor() {
            this.config = {};
        }
        
        setConfig(key, value) {
            this.config[key] = value;
        }
        
        getConfig(key) {
            return this.config[key];
        }
    }
    
    return {
        getInstance: function() {
            if (!instance) {
                instance = new Config();
            }
            return instance;
        }
    };
})();

// 使用示例
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();

console.log(config1 === config2); // true

实战示例

单例模式通常适用于以下场景:

  1. 全局状态管理:配置管理器、数据存储、主题管理
  2. 资源共享:数据库连接池、线程池、缓存
  3. 系统组件:模态框、全局通知、登录框

现在我来举例演示一下面试中常被问到的两种实践方案

本地存储管理器(Storage)

静态方法版
// 定义Storage
class Storage {
    static getInstance() {
        // 判断是否已经new过1个实例
        if (!Storage.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            Storage.instance = new Storage()
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return Storage.instance
    }
    getItem (key) {
        return localStorage.getItem(key)
    }
    setItem (key, value) {
        return localStorage.setItem(key, value)
    }
}

const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2
闭包版
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
    return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
    return localStorage.setItem(key, value)
}

// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function(){
    let instance = null
    return function(){
        // 判断自由变量是否为null
        if(!instance) {
            // 如果为null则new出唯一实例
            instance = new StorageBase()
        }
        return instance
    }
})()

// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果 
const storage1 = new Storage()
const storage2 = new Storage()

storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')

// 返回true
storage1 === storage2

全局模态框(Modal)

这道题比较经典,基本上所有讲单例模式的文章都会以此为例,同时它也是早期单例模式在前端领域的最集中体现

静态方法版
class Modal {
    static instance = null;
    
    constructor() {
        if (Modal.instance) {
            return Modal.instance;
        }
        
        this.modalElement = null;
        this.createModal();
        Modal.instance = this;
    }
    
    createModal() {
        this.modalElement = document.createElement('div');
        this.modalElement.style.display = 'none';
        document.body.appendChild(this.modalElement);
    }
    
    show(content) {
        this.modalElement.innerHTML = content;
        this.modalElement.style.display = 'block';
    }
    
    hide() {
        this.modalElement.style.display = 'none';
    }
}

// 使用示例
const modal1 = new Modal();
const modal2 = new Modal();

modal1.show('Hello World!');
console.log(modal1 === modal2); // true
闭包版
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单例模式弹框</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
	<button id='open'>打开弹框</button>
	<button id='close'>关闭弹框</button>
</body>
<script>
    // 核心逻辑,这里采用了闭包思路来实现单例模式
    const Modal = (function() {
    	let modal = null
    	return function() {
            if(!modal) {
            	modal = document.createElement('div')
            	modal.innerHTML = '我是一个全局唯一的Modal'
            	modal.id = 'modal'
            	modal.style.display = 'none'
            	document.body.appendChild(modal)
            }
            return modal
    	}
    })()
    
    // 点击打开按钮展示模态框
    document.getElementById('open').addEventListener('click', function() {
        // 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
    	const modal = new Modal()
    	modal.style.display = 'block'
    })
    
    // 点击关闭按钮隐藏模态框
    document.getElementById('close').addEventListener('click', function() {
    	const modal = new Modal()
    	if(modal) {
    	    modal.style.display = 'none'
    	}
    })
</script>
</html>

为什么说闭包实现单例模式更安全?【拓展】

1. 构造函数方式可能被篡改:

构造函数方式:

const c1 = new ConfigManager();
ConfigManager.instance = null;  // 破坏单例状态
const c2 = new ConfigManager(); // 会创建新实例
console.log(c1 === c2); // false

这种方式的问题:

  • instance 属性虽然是静态的,但仍可以被外部访问和修改
  • 可以通过 Singleton.instance = null 破坏单例

闭包方式无法篡改:

const c1 = ConfigManager.getInstance();
// 无法直接访问或修改 instance 变量
const c2 = ConfigManager.getInstance();
console.log(s1 === s2); // true

优势:

  • instance 变量完全私有,外部无法访问
  • 只能通过 getInstance 方法获取实例
  • 实例的创建过程被完全封装

2. 初始化控制更严格

闭包比构造函数有更灵活的初始化时机,让我用一个具体的例子来解释这句话

假设我们有一个数据库连接的单例:

// 构造函数方式
class DatabaseConnection {
    static instance = null;
    
    constructor() {
        if (DatabaseConnection.instance) {
            return DatabaseConnection.instance;
        }
        this.connect();  // 连接数据库
        DatabaseConnection.instance = this;
    }
    
    connect() {
        // 建立数据库连接
        console.log('建立数据库连接...');
    }
}

// 在程序启动时就创建实例
const db = new DatabaseConnection();

这种方式的问题是:即使我们还没有用到数据库,程序一启动就会创建连接,浪费了系统资源。

而使用闭包的方式:

// 闭包方式
const DatabaseConnection = (function() {
    let instance = null;
    
    function init() {
        // 只有在真正需要时才建立连接
        return {
            connect() {
                console.log('建立数据库连接...');
            }
        };
    }
    
    return {
        getInstance() {
            if (!instance) {
                instance = init();  // 延迟到第一次调用时才初始化
            }
            return instance;
        }
    };
})();

// 只有在真正需要时才获取实例
const db = DatabaseConnection.getInstance();

这种方式的优势:

  1. 实例只在第一次调用 getInstance 时才创建
  2. 避免了资源的浪费,这就是所谓的"懒加载"(Lazy Loading)策略,它确保了实例只在真正需要时才被创建,而不是在程序一启动就创建。这在处理资源密集型的对象(如数据库连接、文件系统等)时特别有用。

单例模式的优缺点

优点:

  1. 保证一个类只有一个实例
  2. 提供了对该实例的全局访问点
  3. 节约系统资源
  4. 避免对共享资源的多重占用

缺点:

  1. 违反单一职责原则
  2. 可能掩盖不良设计
  3. 在并发环境下需要特别注意线程安全
  4. 测试可能会变得困难

总结

单例模式是一种简单但强大的设计模式,它在需要确保全局唯一性的场景中非常有用。但要注意,过度使用单例可能导致代码耦合度过高,因此在使用时需要权衡利弊,选择合适的场景。