《JavaScript单例模式详解:从原理到实践》

前言

在软件开发中,设计模式是解决特定问题的经典方案。今天我们要探讨的是单例模式(Singleton Pattern),这是一种创建型设计模式,也是JavaScript中最常用的设计模式之一。无论你是前端新手还是资深开发者,理解单例模式都能让你的代码更加优雅高效。

介绍单例模式

单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点。就像公司里的CEO职位,无论多少人问"谁是CEO",得到的都是同一个对象。

举个栗子!

非单例模式:

js 复制代码
//传统模式
class Dog {
  constructor(name, breed, age) {
    this.name = name;
    this.breed = breed;
    this.age = age;
  }
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);

console.log(dog1 === dog2) //false

单例模式:

js 复制代码
//单例模式
class Dog {
  constructor(name, breed, age) {
    if (Dog.instance) {
      return Dog.instance;
    }
    
    this.name = name;
    this.breed = breed;
    this.age = age;
    
    Dog.instance = this;
  }
}
const dog1 = new Dog('Buddy', 'Golden Retriever', 3);
const dog2 = new Dog('Luna', 'Husky', 2);

console.log(dog1 === dog2);  // true,两个变量引用同一个实例

为什么需要单例?

  • 避免重复创建消耗资源的对象
  • 确保全局状态的一致性
  • 提供对唯一实例的受控访问

在JavaScript中,单例模式常用于:

  • 全局状态管理(如Redux store)
  • 缓存系统
  • 日志记录器
  • 浏览器中的window对象本身就是单例的典型例子

如何实现

下面我们通过一个本地存储封装的示例来演示单例模式的实现:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>设计模式之单例模式</title>
</head>
<body>
    <script>
        /*
         * Storage 本地存储类
         * 单例模式实现
         * @func getItem 获取本地的值
         * @func setItem 设置本地存储的值
         */
        class Storage {
            // 构造函数,可以指定命名空间防止键名冲突
            constructor(namespace = 'storage') {
                this.namespace = namespace;
            }

            // 静态方法获取单例实例
            static getInstance(){
                // 检查是否已存在实例
                if (!Storage.instance) {
                    // 不存在则创建新实例
                    Storage.instance = new Storage();
                }
                // 返回已存在的实例
                return Storage.instance;
            }    
            
            // 获取本地存储的值
            getItem(key) {
                return localStorage.getItem(this.namespace + key);
            }   
    
            // 设置本地存储的值
            setItem(key, value) {
                localStorage.setItem(this.namespace + key, value);
            }
        }

        // 测试单例模式
        const storage1 = Storage.getInstance();
        const storage2 = Storage.getInstance();

        // 验证两个变量是否指向同一个实例
        console.log(storage1 === storage2, '!!!');  // 输出: true '!!!'
        
        // 通过一个实例设置值
        storage1.setItem('name','haha');
        
        // 通过另一个实例获取值
        console.log(storage1.getItem('name'));  // 输出: "haha"
        console.log(storage2.getItem('name'));  // 输出: "haha"
    </script>
</body>
</html>

代码解读:

  1. 我们创建了一个Storage类来封装localStorage操作

  2. 关键点在于getInstance静态方法:

    • 它检查类属性Storage.instance是否存在
    • 不存在时才会创建新实例
    • 确保无论调用多少次getInstance()都返回同一个实例
  3. 测试代码验证了storage1storage2确实是同一个实例

  4. 并且通过一个实例修改的值,可以从另一个实例正确获取

用闭包实现单例

这里再提一种用闭包实现单例的方法,直接看代码

js 复制代码
// 1. 基础构造函数
function StorageBase() {
    // 空构造函数
}

// 2. 在原型上添加方法
StorageBase.prototype.getItem = function(key) {
    return localStorage.getItem(key);
}

StorageBase.prototype.setItem = function(key, value) {  // 注意:参数列表中缺少value
    return localStorage.setItem(key, value);  // 修正:原代码是settItem拼写错误
}

// 3. 单例封装(核心部分)
const Storage = (function() {
    let instance = null;  // 闭包保存单例实例
    
    return function() {   // 返回真正的构造函数
        if(!instance) {
            instance = new StorageBase();  // 首次调用时创建实例
        }
        return instance;  // 总是返回同一个实例
    }
})();  // 立即执行

// 4. 测试
const storage1 = Storage();
const storage2 = Storage();
console.log(storage1 === storage2)  // true

这是一种es6之前,没有class语法糖的写法: 使用了闭包+立即执行函数的方式来实现单例模式,是一种经典的实现方式。下面我们逐部分解析:

  1. 闭包保存单例实例

    • 使用IIFE(立即执行函数)创建闭包环境
    • instance变量被闭包保护,外部无法直接访问
  2. 惰性初始化(Lazy Initialization)

    • 只有第一次调用Storage()时才会创建实例
    • 后续调用直接返回已创建的实例
  3. 原型方法

    • 将方法挂在StorageBase.prototype
    • 所有实例共享这些方法(虽然这里只有一个实例)

与之前的class方式实现相比,两种方式的区别:

特性 类实现 闭包函数实现
实例存储位置 类的静态属性 闭包变量
实现方式 class语法 构造函数+原型链
访问方式 静态方法getInstance() 直接函数调用
现代性 ES6+ ES5
私有性 较弱 闭包提供更好的封装

单例模式的重要性

单例模式在前端开发里可太重要啦,为啥这么说呢,因为它能解决下面这些问题:

  1. 资源优化:像数据库连接、WebSocket 连接这些老占资源的对象,用它就能避免反复创建
  2. 状态一致性:能保证像用户信息这种全局状态,在整个应用里都保持一致
  3. 内存管理:能减少那些没必要的内存占用,特别是在像移动端这种资源不咋够的环境里
  4. 访问控制:弄个统一的访问入口,以后要是想加日志、做验证啥的逻辑,都方便

实际应用场景:

  • 全局状态管理库(如Redux、Vuex的store)
  • 模态框/对话框管理
  • 缓存系统
  • 日志记录器
  • 浏览器中的window/document对象

注意事项:

  • 单例模式可能引入全局状态,过度使用会导致代码难以测试
  • 在JavaScript中要考虑线程安全(虽然JS是单线程的)
  • 考虑使用依赖注入来改善可测试性

通过合理使用单例模式,我们可以构建出更加健壮、高效的JavaScript应用。

相关推荐
Dnui_King1 分钟前
Oracle 在线重定义
java·服务器·前端
西柚啊13 分钟前
AI + 可视化:Stagewise 如何让前端 UI 调试效率飞跃
前端·ai编程
中微子15 分钟前
解决跨域问题的古早方法JSONP
前端
嘉琪00117 分钟前
2025——js面试题(9)——开发环境相关
开发语言·javascript·ecmascript
林太白33 分钟前
Rust详情修改删除优化
前端·后端·rust
HANK37 分钟前
前端自动化部署全流程实战指南
前端
Neon120440 分钟前
Nuxt.js 国际化配置完整教程(含版本兼容与问题解决)
前端·nuxt.js
薛定谔的算法42 分钟前
React 项目骨架把“一万行灾难”拆成“百来个各司其职的小文件”
前端·react.js·前端框架
Keya1 小时前
鸿蒙开发样式复用:@Styles、@Extend与AttributeModifier深度对比
前端·分布式·harmonyos
拉不动的猪1 小时前
JS篇之性能优化
前端·javascript·面试